index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <script setup lang="ts">
  2. import { useI18n } from "vue-i18n";
  3. import Motion from "./utils/motion";
  4. import { useRouter } from "vue-router";
  5. import { message } from "@/utils/message";
  6. import { loginRules } from "./utils/rule";
  7. import TypeIt from "@/components/ReTypeit";
  8. import { debounce } from "@pureadmin/utils";
  9. import { useNav } from "@/layout/hooks/useNav";
  10. import { useEventListener } from "@vueuse/core";
  11. import type { FormInstance } from "element-plus";
  12. import { $t, transformI18n } from "@/plugins/i18n";
  13. import { operates, thirdParty } from "./utils/enums";
  14. import { useLayout } from "@/layout/hooks/useLayout";
  15. import LoginPhone from "./components/LoginPhone.vue";
  16. import LoginRegist from "./components/LoginRegist.vue";
  17. import LoginUpdate from "./components/LoginUpdate.vue";
  18. import LoginQrCode from "./components/LoginQrCode.vue";
  19. import { useUserStoreHook } from "@/store/modules/user";
  20. import { initRouter, getTopMenu } from "@/router/utils";
  21. import { bg, avatar, illustration } from "./utils/static";
  22. import { ReImageVerify } from "@/components/ReImageVerify";
  23. import { ref, toRaw, reactive, watch, computed, onMounted } from "vue";
  24. import { useRenderIcon } from "@/components/ReIcon/src/hooks";
  25. import { useTranslationLang } from "@/layout/hooks/useTranslationLang";
  26. import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
  27. import dayIcon from "@/assets/svg/day.svg?component";
  28. import darkIcon from "@/assets/svg/dark.svg?component";
  29. import globalization from "@/assets/svg/globalization.svg?component";
  30. import Lock from "~icons/ri/lock-fill";
  31. import Check from "~icons/ep/check";
  32. import User from "~icons/ri/user-3-fill";
  33. import Info from "~icons/ri/information-line";
  34. import Keyhole from "~icons/ri/shield-keyhole-line";
  35. import { fetchSysCfg } from "@/api/system";
  36. import { ElNotification } from "element-plus";
  37. defineOptions({
  38. name: "Login"
  39. });
  40. const imgCode = ref("");
  41. const loginDay = ref(7);
  42. const router = useRouter();
  43. const loading = ref(false);
  44. const checked = ref(false);
  45. const disabled = ref(false);
  46. const ruleFormRef = ref<FormInstance>();
  47. const currentPage = computed(() => {
  48. return useUserStoreHook().currentPage;
  49. });
  50. const { t } = useI18n();
  51. const { initStorage } = useLayout();
  52. initStorage();
  53. const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
  54. dataThemeChange(overallStyle.value);
  55. const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
  56. const { locale, translationCh, translationEn } = useTranslationLang();
  57. const ruleForm = reactive({
  58. username: "root",
  59. password: "zaq123!@#",
  60. verifyCode: ""
  61. });
  62. const vGlobal = window.vueGlobal;
  63. const tiId = ref(0);
  64. onMounted(() => {
  65. // 组件挂载后执行的逻辑
  66. fetchSysCfg({})
  67. .then(response => {
  68. console.log("系统配置项:", response);
  69. if (response.success) {
  70. vGlobal.sysCfg.init(response.dict);
  71. vGlobal.applySysCfg(true);
  72. tiId.value = tiId.value + 1;
  73. } else {
  74. ElNotification.error({
  75. message: t("login.loadConfigFail")
  76. });
  77. }
  78. })
  79. .catch(error => {
  80. console.error("配置加载失败", error);
  81. ElNotification.error({
  82. message: t("login.networkError")
  83. });
  84. });
  85. });
  86. const webTitle = computed(() => {
  87. return vGlobal.sysCfg.sysCfgView.title;
  88. });
  89. const onLogin = async (formEl: FormInstance | undefined) => {
  90. if (!formEl) return;
  91. await formEl.validate(valid => {
  92. if (valid) {
  93. loading.value = true;
  94. useUserStoreHook()
  95. .loginByUsername({
  96. username: ruleForm.username,
  97. password: ruleForm.password
  98. })
  99. .then(res => {
  100. if (res.success) {
  101. // 获取后端路由
  102. return initRouter().then(() => {
  103. disabled.value = true;
  104. router
  105. .push(getTopMenu(true).path)
  106. .then(() => {
  107. // message(t("login.pureLoginSuccess"), { type: "success" });
  108. ElNotification.success({
  109. message: t("login.pureLoginSuccess")
  110. });
  111. })
  112. .finally(() => (disabled.value = false));
  113. });
  114. } else {
  115. ElNotification.error({
  116. message: t("login.pureLoginFail") + ":" + t(`login.${res.error}`)
  117. });
  118. // message(t("login.pureLoginFail"), { type: "error" });
  119. }
  120. })
  121. .finally(() => (loading.value = false));
  122. }
  123. });
  124. };
  125. const immediateDebounce: any = debounce(
  126. formRef => onLogin(formRef),
  127. 1000,
  128. true
  129. );
  130. useEventListener(document, "keydown", ({ code }) => {
  131. if (
  132. ["Enter", "NumpadEnter"].includes(code) &&
  133. !disabled.value &&
  134. !loading.value
  135. )
  136. immediateDebounce(ruleFormRef.value);
  137. });
  138. watch(imgCode, value => {
  139. useUserStoreHook().SET_VERIFYCODE(value);
  140. });
  141. watch(checked, bool => {
  142. useUserStoreHook().SET_ISREMEMBERED(bool);
  143. });
  144. watch(loginDay, value => {
  145. useUserStoreHook().SET_LOGINDAY(value);
  146. });
  147. </script>
  148. <template>
  149. <div class="select-none">
  150. <img :src="bg" class="wave" />
  151. <div class="flex-c absolute right-5 top-3 custom-position">
  152. <!-- 主题 -->
  153. <el-switch
  154. v-model="dataTheme"
  155. inline-prompt
  156. :active-icon="dayIcon"
  157. :inactive-icon="darkIcon"
  158. @change="dataThemeChange"
  159. />
  160. <!-- 国际化 -->
  161. <!-- <el-dropdown trigger="click">
  162. <globalization
  163. class="hover:text-primary hover:bg-[transparent]! w-[20px] h-[20px] ml-1.5 cursor-pointer outline-hidden duration-300"
  164. />
  165. <template #dropdown>
  166. <el-dropdown-menu class="translation">
  167. <el-dropdown-item
  168. :style="getDropdownItemStyle(locale, 'zh')"
  169. :class="['dark:text-white!', getDropdownItemClass(locale, 'zh')]"
  170. @click="translationCh"
  171. >
  172. <IconifyIconOffline
  173. v-show="locale === 'zh'"
  174. class="check-zh"
  175. :icon="Check"
  176. />
  177. 简体中文
  178. </el-dropdown-item>
  179. <el-dropdown-item
  180. :style="getDropdownItemStyle(locale, 'en')"
  181. :class="['dark:text-white!', getDropdownItemClass(locale, 'en')]"
  182. @click="translationEn"
  183. >
  184. <span v-show="locale === 'en'" class="check-en">
  185. <IconifyIconOffline :icon="Check" />
  186. </span>
  187. English
  188. </el-dropdown-item>
  189. </el-dropdown-menu>
  190. </template>
  191. </el-dropdown> -->
  192. </div>
  193. <div class="login-container">
  194. <div class="img">
  195. <component :is="toRaw(illustration)" />
  196. </div>
  197. <div class="login-box">
  198. <div class="login-form">
  199. <avatar class="avatar" />
  200. <Motion :tiId="tiId">
  201. <h2 class="outline-hidden">
  202. {{ vGlobal.sysCfg.sysCfgView.webTitle }}
  203. <!-- <TypeIt
  204. :tiId="tiId"
  205. :options="{ strings: [vGlobal.sysCfg.sysCfgView.webTitle], cursor: false, speed: 100 }"
  206. /> -->
  207. </h2>
  208. </Motion>
  209. <el-form
  210. v-if="currentPage === 0"
  211. ref="ruleFormRef"
  212. :model="ruleForm"
  213. :rules="loginRules"
  214. size="large"
  215. >
  216. <Motion :delay="100">
  217. <el-form-item
  218. :rules="[
  219. {
  220. required: true,
  221. message: transformI18n($t('login.pureUsernameReg')),
  222. trigger: 'blur'
  223. }
  224. ]"
  225. prop="username"
  226. >
  227. <el-input
  228. v-model="ruleForm.username"
  229. clearable
  230. :placeholder="t('login.pureUsername')"
  231. :prefix-icon="useRenderIcon(User)"
  232. />
  233. </el-form-item>
  234. </Motion>
  235. <Motion :delay="150">
  236. <el-form-item prop="password">
  237. <el-input
  238. v-model="ruleForm.password"
  239. clearable
  240. show-password
  241. :placeholder="t('login.purePassword')"
  242. :prefix-icon="useRenderIcon(Lock)"
  243. />
  244. </el-form-item>
  245. </Motion>
  246. <Motion :delay="200">
  247. <el-form-item prop="verifyCode">
  248. <el-input
  249. v-model="ruleForm.verifyCode"
  250. clearable
  251. :placeholder="t('login.pureVerifyCode')"
  252. :prefix-icon="useRenderIcon(Keyhole)"
  253. >
  254. <template v-slot:append>
  255. <ReImageVerify v-model:code="imgCode" />
  256. </template>
  257. </el-input>
  258. </el-form-item>
  259. </Motion>
  260. <Motion :delay="250">
  261. <el-form-item>
  262. <div
  263. class="w-full h-[20px] flex justify-between items-center custom-flex-container"
  264. >
  265. <el-checkbox v-model="checked">
  266. <span class="flex">
  267. <select
  268. v-model="loginDay"
  269. :style="{
  270. width: loginDay < 10 ? '10px' : '16px',
  271. outline: 'none',
  272. background: 'none',
  273. '-webkit-appearance': 'none',
  274. appearance: 'none',
  275. border: 'none'
  276. }"
  277. >
  278. <option value="1">1</option>
  279. <option value="7">7</option>
  280. <option value="30">30</option>
  281. </select>
  282. {{ t("login.pureRemember") }}
  283. <IconifyIconOffline
  284. v-tippy="{
  285. content: t('login.pureRememberInfo'),
  286. placement: 'top'
  287. }"
  288. :icon="Info"
  289. class="ml-1"
  290. />
  291. </span>
  292. </el-checkbox>
  293. <el-button
  294. link
  295. type="primary"
  296. @click="useUserStoreHook().SET_CURRENTPAGE(4)"
  297. >
  298. {{ t("login.pureForget") }}
  299. </el-button>
  300. </div>
  301. <el-button
  302. class="w-full mt-4"
  303. size="default"
  304. type="primary"
  305. :loading="loading"
  306. :disabled="disabled"
  307. @click="onLogin(ruleFormRef)"
  308. >
  309. {{ t("login.pureLogin") }}
  310. </el-button>
  311. </el-form-item>
  312. </Motion>
  313. <!-- <Motion :delay="300">
  314. <el-form-item>
  315. <div class="w-full h-[20px] flex justify-between items-center">
  316. <el-button
  317. v-for="(item, index) in operates"
  318. :key="index"
  319. class="w-full mt-4"
  320. size="default"
  321. @click="useUserStoreHook().SET_CURRENTPAGE(index + 1)"
  322. >
  323. {{ t(item.title) }}
  324. </el-button>
  325. </div>
  326. </el-form-item>
  327. </Motion> -->
  328. </el-form>
  329. <!-- <Motion v-if="currentPage === 0" :delay="350">
  330. <el-form-item>
  331. <el-divider>
  332. <p class="text-gray-500 text-xs">
  333. {{ t("login.pureThirdLogin") }}
  334. </p>
  335. </el-divider>
  336. <div class="w-full flex justify-evenly">
  337. <span
  338. v-for="(item, index) in thirdParty"
  339. :key="index"
  340. :title="t(item.title)"
  341. >
  342. <IconifyIconOnline
  343. :icon="`ri:${item.icon}-fill`"
  344. width="20"
  345. class="cursor-pointer text-gray-500 hover:text-blue-400"
  346. />
  347. </span>
  348. </div>
  349. </el-form-item>
  350. </Motion> -->
  351. <!-- 手机号登录 -->
  352. <!-- <LoginPhone v-if="currentPage === 1" /> -->
  353. <!-- 二维码登录 -->
  354. <!-- <LoginQrCode v-if="currentPage === 2" /> -->
  355. <!-- 注册 -->
  356. <!-- <LoginRegist v-if="currentPage === 3" /> -->
  357. <!-- 忘记密码 -->
  358. <!-- <LoginUpdate v-if="currentPage === 4" /> -->
  359. </div>
  360. </div>
  361. </div>
  362. <div
  363. class="w-full flex-c absolute bottom-3 text-sm text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]"
  364. >
  365. Copyright © 2020-present
  366. </div>
  367. </div>
  368. </template>
  369. <style scoped>
  370. @import url("@/style/login.css");
  371. </style>
  372. <style lang="scss" scoped>
  373. :deep(.el-input-group__append, .el-input-group__prepend) {
  374. padding: 0;
  375. }
  376. .translation {
  377. :deep(.el-dropdown-menu__item) {
  378. padding: 5px 40px;
  379. }
  380. .check-zh {
  381. position: absolute;
  382. left: 20px;
  383. }
  384. .check-en {
  385. position: absolute;
  386. left: 20px;
  387. }
  388. }
  389. .custom-position {
  390. right: 1.25rem; /* 等价于 right-5 */
  391. top: 0.75rem; /* 等价于 top-3 */
  392. }
  393. .flex-c {
  394. display: -webkit-box;
  395. display: -ms-flexbox;
  396. display: flex;
  397. -webkit-box-align: center;
  398. -ms-flex-align: center;
  399. align-items: center;
  400. -webkit-box-pack: center;
  401. -ms-flex-pack: center;
  402. justify-content: center;
  403. }
  404. .select-none {
  405. position: relative; /* 确保子元素 absolute 定位以此为基准 */
  406. }
  407. .absolute {
  408. position: absolute; /* 脱离文档流,基于最近的定位父元素偏移 */
  409. }
  410. /* 替代原工具类组合:w-full h-[20px] flex justify-between items-center */
  411. .custom-flex-container {
  412. /* w-full:宽度占满父容器 */
  413. width: 100%;
  414. /* h-[20px]:固定高度20px */
  415. height: 20px;
  416. /* flex:启用flex布局 */
  417. display: -webkit-box; /* 兼容旧版WebKit浏览器 */
  418. display: -ms-flexbox; /* 兼容旧版IE */
  419. display: flex;
  420. /* justify-between:子元素水平方向两端对齐 */
  421. -webkit-box-pack: justify;
  422. -ms-flex-pack: justify;
  423. justify-content: space-between;
  424. /* items-center:子元素垂直方向居中对齐 */
  425. -webkit-box-align: center;
  426. -ms-flex-align: center;
  427. align-items: center;
  428. }
  429. .flex {
  430. display: flex;
  431. }
  432. .w-full {
  433. /* w-full:宽度为父容器的 100% */
  434. width: 100%;
  435. }
  436. .mt-4 {
  437. /* mt-4:上外边距,Tailwind 中 1 单位 = 0.25rem,4 单位即 1rem(16px,默认根字体大小下) */
  438. margin-top: 1rem;
  439. }
  440. .ml-1-custom {
  441. /* Tailwind 中 1 单位 = 0.25rem,ml-1 即左外边距为 0.25rem */
  442. margin-left: 0.25rem;
  443. }
  444. .bottom-3 {
  445. /* bottom-3:Tailwind 中 1 单位 = 0.25rem,3 单位即 0.75rem */
  446. bottom: 0.75rem;
  447. }
  448. .text-sm {
  449. /* text-sm:Tailwind 默认字体大小为 0.875rem(14px,基于 1rem=16px 计算) */
  450. font-size: 0.875rem;
  451. }
  452. </style>