loginUtils.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. import {computed, ref} from 'vue'
  2. import {
  3. getTenantIdByName,
  4. login,
  5. logout,
  6. smsCode,
  7. smsLogin
  8. } from '@/api/login/login.js'
  9. import {ElLoading, ElMessage} from 'element-plus'
  10. import {refreshAllDictData} from './dictUtils.js';
  11. // CryptoJS 库(用于密码加密)
  12. import CryptoJS from 'crypto-js';
  13. // 消息工具
  14. import {Message} from './message/Message.js';
  15. import {CONFIG, refreshRoleRoute} from "@/utils/roleUtils.js";
  16. import {homeRoutes} from "@/router/index.js";
  17. // 加密密钥 (从环境变量获取)
  18. const SECRET_KEY = import.meta.env.VITE_SECRET_KEY;
  19. // 是否开启租户模式
  20. const tenantOpen = import.meta.env.VITE_APP_COMPULSORY_TENANT === 'true';
  21. //需要删除的缓存key列表
  22. const CACHE_KEYS_TO_DELETE = [
  23. 'userId',
  24. 'token',
  25. 'isLoggedIn',
  26. CONFIG.USER_ROLE_ROUTE_KEY,
  27. CONFIG.USER_ROLE_ROUTE_KEY + CONFIG.EXPIRY_SUFFIX,
  28. ];
  29. // 密码加密函数(使用AES加密)
  30. const encryptPassword = (password) => {
  31. // 将加密结果转换为字符串并返回 (encrypt加密)
  32. return CryptoJS.AES.encrypt(password, SECRET_KEY).toString();
  33. };
  34. // 密码解密函数 (接收加密后的密码字符串作为参数)
  35. const decryptPassword = (encryptedPassword) => {
  36. // (decrypt解密)
  37. const bytes = CryptoJS.AES.decrypt(encryptedPassword, SECRET_KEY);
  38. // 将解密结果转换为 UTF-8 编码的字符串并返回
  39. return bytes.toString(CryptoJS.enc.Utf8);
  40. };
  41. // 登录数据结构
  42. const createLoginData = () => {
  43. return ref({
  44. loginForm: {
  45. tenantName: tenantOpen ? (import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '') : null,
  46. username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
  47. password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
  48. smsCode: '', // 短信验证码字段
  49. rememberMe: false, // 记住
  50. phoneNumber: '' // 手机号字段
  51. }
  52. })
  53. }
  54. // 验证码相关逻辑
  55. const createVerificationCodeLogic = () => {
  56. const countingDown = ref(false)
  57. const countDown = ref(60)
  58. const sendingCode = ref(false)
  59. let countDownTimer = null
  60. // 开始倒计时
  61. const startCountDown = () => {
  62. countingDown.value = true
  63. countDown.value = 60
  64. if (countDownTimer) {
  65. clearInterval(countDownTimer)
  66. }
  67. countDownTimer = setInterval(() => {
  68. countDown.value--
  69. if (countDown.value <= 0) {
  70. clearInterval(countDownTimer)
  71. countingDown.value = false
  72. }
  73. }, 1000)
  74. }
  75. // 清除计时器
  76. const clearCountDownTimer = () => {
  77. if (countDownTimer) {
  78. clearInterval(countDownTimer)
  79. countDownTimer = null
  80. }
  81. }
  82. // 获取短信验证码
  83. const getSmsCode = async (tenantId, tenantName, mobile) => {
  84. sendingCode.value = true
  85. try {
  86. // 构建请求头,只有当 tenantId 存在时才添加 Tenant-Id
  87. const headers = tenantId ? { 'Tenant-Id': tenantId } : {}
  88. const res = await smsCode(
  89. headers, {
  90. tenantName,
  91. mobile,
  92. scene: import.meta.env.VITE_APP_LOGIN_SMS_TEMPLATE_ID,
  93. }
  94. )
  95. if (res.code === 0) {
  96. ElMessage.success('验证码发送成功')
  97. startCountDown()
  98. } else {
  99. ElMessage.error(res.message || '验证码发送失败')
  100. }
  101. } catch (error) {
  102. ElMessage.error('验证码发送失败,请重试')
  103. console.error('发送验证码错误:', error)
  104. } finally {
  105. sendingCode.value = false
  106. }
  107. }
  108. return {
  109. countingDown,
  110. countDown,
  111. sendingCode,
  112. getSmsCode,
  113. clearCountDownTimer
  114. }
  115. }
  116. // 获取租户ID
  117. const getTenantId = async (tenantName) => {
  118. try {
  119. const res = await getTenantIdByName(tenantName)
  120. if (res && res.data) {
  121. return res.data
  122. } else {
  123. ElMessage.error('租户填写错误!')
  124. return null
  125. }
  126. } catch (error) {
  127. ElMessage.error('租户填写错误!')
  128. console.error('获取租户 ID 错误:', error)
  129. return null
  130. }
  131. }
  132. // 登录逻辑
  133. const loginLogic = async (loginForm, tenantId, isAuthorized, router, redirectPath) => {
  134. const loginLoading = ref(false)
  135. const loading = ref()
  136. const isLoggedIn = ref(false)
  137. loginLoading.value = true
  138. try {
  139. let res
  140. // 构建请求头,只有当 tenantId 存在时才添加 Tenant-Id
  141. const headers = tenantId ? { 'Tenant-Id': tenantId } : {}
  142. if (!isAuthorized.value) {
  143. // 未授权状态,使用短信验证码登录
  144. res = await smsLogin(
  145. headers, {
  146. mobile: loginForm.phoneNumber,
  147. code: loginForm.smsCode,
  148. }
  149. )
  150. } else {
  151. // 授权状态,使用账号密码登录
  152. // 传输时使用明文密码,确保后端能正确处理
  153. res = await login(
  154. headers,
  155. loginForm
  156. )
  157. }
  158. if (!res) {
  159. return false
  160. }
  161. // 校验登录状态
  162. if (res.code === 0) {
  163. // ElMessage.success('登录成功')
  164. isLoggedIn.value = true
  165. // 存储登录状态
  166. localStorage.setItem('isLoggedIn', 'true')
  167. localStorage.setItem('token', res.data.accessToken)
  168. // 登录成功后存储用户ID
  169. if (res.data.userId) {
  170. localStorage.setItem('userId', res.data.userId)
  171. }
  172. // 总是存储用户名和租户名称
  173. localStorage.setItem('tenantName', res.data.tenantName)
  174. // 存储记住我状态
  175. localStorage.setItem('rememberMe', loginForm.rememberMe)
  176. if (loginForm.rememberMe) {
  177. // 保存加密后的密码
  178. localStorage.setItem('password', encryptPassword(loginForm.password))
  179. } else {
  180. // 如果没有勾选记住我,清除密码
  181. localStorage.removeItem('password')
  182. }
  183. loading.value = ElLoading.service({
  184. lock: true,
  185. text: '正在加载系统中...',
  186. background: 'rgba(0, 0, 0, 0.7)'
  187. })
  188. // 获取字典数据
  189. await refreshDictData();
  190. // 获取角色路由数据
  191. await refreshRoleRouteData();
  192. // 登录成功后,跳转到指定的页面
  193. router.push(redirectPath)
  194. return true
  195. } else if (res.code === 1002000009) {
  196. // 未授权状态,切换到短信验证码登录
  197. ElMessage.warning(res.msg || '登录IP未被授权,请使用手机号短信验证码登录!')
  198. loginForm.phoneNumber = res.data?.mobile
  199. isAuthorized.value = false
  200. return false
  201. } else {
  202. ElMessage.error(res.msg || '登录失败,请检查账号密码!')
  203. return false
  204. }
  205. } catch (error) {
  206. ElMessage.error('登录出错,请重试!')
  207. console.error('登录错误:', error)
  208. return false
  209. } finally {
  210. loginLoading.value = false
  211. if (loading.value) {
  212. loading.value.close()
  213. }
  214. }
  215. }
  216. // 本地存储管理
  217. const loadLoginData = (loginData) => {
  218. const storedTenantName = localStorage.getItem('tenantName')
  219. const storedUserName = localStorage.getItem('userName')
  220. const storedPassword = localStorage.getItem('password')
  221. // 恢复登录信息到输入框
  222. if (tenantOpen && storedTenantName) {
  223. loginData.value.loginForm.tenantName = storedTenantName
  224. }
  225. if (storedUserName) {
  226. loginData.value.loginForm.username = storedUserName
  227. }
  228. if (storedPassword) {
  229. // 解密密码并显示明文
  230. loginData.value.loginForm.password = decryptPassword(storedPassword)
  231. loginData.value.loginForm.rememberMe = true
  232. }
  233. }
  234. // 检查登录状态
  235. const checkLoginStatus = (router, redirectPath) => {
  236. const storedStatus = localStorage.getItem('isLoggedIn')
  237. if (storedStatus === 'true') {
  238. // 获取字典数据
  239. refreshDictData();
  240. // 获取角色路由数据
  241. refreshRoleRouteData();
  242. router.push(redirectPath)
  243. return true
  244. }
  245. return false
  246. }
  247. // 表单验证规则生成
  248. const generateRules = (isAuthorized) => {
  249. return computed(() => {
  250. if (isAuthorized.value) {
  251. // 授权状态:需要账号和密码
  252. const rules = {
  253. username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
  254. password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
  255. }
  256. if (tenantOpen) {
  257. rules.tenantName = [{ required: true, message: '请输入学校名称', trigger: 'blur' }]
  258. }
  259. return rules
  260. } else {
  261. // 未授权状态:需要手机号和短信验证码
  262. const rules = {
  263. phoneNumber: [
  264. { required: true, message: '请输入手机号', trigger: 'blur' },
  265. { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
  266. ],
  267. smsCode: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }]
  268. }
  269. if (tenantOpen) {
  270. rules.tenantName = [{ required: true, message: '请输入学校名称', trigger: 'blur' }]
  271. }
  272. return rules
  273. }
  274. })
  275. }
  276. // 自动登录逻辑(用于QuickLogin和PromotionLogin)
  277. const autoLogin = async (tenantName, username, password, router, redirectPath) => {
  278. let loading = null
  279. try {
  280. // 显示全局加载状态
  281. loading = ElLoading.service({
  282. lock: true,
  283. text: '登录中...',
  284. background: 'rgba(0, 0, 0, 0.7)'
  285. })
  286. // 获取租户ID,只有当 tenantOpen 为 true 时才获取
  287. let tenantId = await getTenantId(tenantName)
  288. if (!tenantId) {
  289. // 租户验证失败
  290. return false
  291. }
  292. // 构建请求头,只有当 tenantId 存在时才添加 Tenant-Id
  293. const headers = tenantId ? { 'Tenant-Id': tenantId } : {}
  294. // 执行登录
  295. const res = await login(
  296. headers,
  297. { tenantName, username, password }
  298. )
  299. if (res && res.code === 0) {
  300. // 登录成功,保存登录状态
  301. localStorage.setItem('isLoggedIn', 'true')
  302. localStorage.setItem('token', res.data.accessToken)
  303. localStorage.setItem('userName', username)
  304. localStorage.setItem('tenantName', tenantName)
  305. localStorage.setItem('password', encryptPassword(password))
  306. localStorage.setItem('rememberMe', 'true')
  307. ElMessage.success('信息校验成功')
  308. // 跳转到课程界面
  309. router.push(redirectPath)
  310. return true
  311. } else {
  312. ElMessage.error(res?.message || '信息校验失败')
  313. // 如果登录失败,跳转到正常登录页面
  314. router.push(homeRoutes.login)
  315. return false
  316. }
  317. } catch (error) {
  318. ElMessage.error('信息校验过程中发生错误')
  319. console.error('信息校验错误:', error)
  320. // 错误时跳转到登录页面
  321. router.push(homeRoutes.login)
  322. return false
  323. } finally {
  324. // 关闭加载状态
  325. if (loading) {
  326. loading.close()
  327. }
  328. }
  329. }
  330. // 退出登录逻辑
  331. const logoutLogic = async (router, redirectPath = homeRoutes.login) => {
  332. try {
  333. // 调用logout API 退出登录
  334. const logoutRes = await logout()
  335. console.log('退出登录:', logoutRes);
  336. //清除本地存储
  337. removeLocalStorage();
  338. // 检查是否勾选了记住我
  339. const rememberMe = localStorage.getItem('rememberMe') === 'true'
  340. if (!rememberMe) {
  341. // 未勾选记住我,清空所有信息
  342. localStorage.removeItem('userName')
  343. localStorage.removeItem('tenantName')
  344. localStorage.removeItem('rememberMe')
  345. }
  346. // 跳转到登录页面
  347. router.push({ path: redirectPath })
  348. } catch (error) {
  349. console.error('退出登录失败:', error)
  350. // API调用失败,也清空本地存储
  351. //清除本地存储
  352. removeLocalStorage();
  353. // 检查是否勾选了记住我
  354. const rememberMe = localStorage.getItem('rememberMe') === 'true'
  355. if (!rememberMe) {
  356. // 未勾选记住我,清空所有信息
  357. localStorage.removeItem('userName')
  358. localStorage.removeItem('tenantName')
  359. localStorage.removeItem('rememberMe')
  360. }
  361. Message().notifyError('退出登录失败,请重试', true)
  362. router.push({ path: homeRoutes.login })
  363. }
  364. }
  365. //清除本地存储
  366. const removeLocalStorage = () => {
  367. // 清空登录状态相关信息
  368. CACHE_KEYS_TO_DELETE.forEach(key => localStorage.removeItem(key));
  369. removeLocalStorageKey(localStorage.getItem('token'))
  370. }
  371. const removeLocalStorageKey = (remKey) => {
  372. if (remKey) {
  373. for (let i = 0; i < localStorage.length; i++) {
  374. const key = localStorage.key(i);
  375. if (key && key.startsWith(remKey)) {
  376. localStorage.removeItem(key);
  377. }
  378. }
  379. }
  380. }
  381. // 添加获取字典数据的函数
  382. const refreshDictData = async () => {
  383. try {
  384. // 使用典管理工具更新所有字典
  385. await refreshAllDictData();
  386. } catch (error) {
  387. console.error('获取字典数据失败:', error);
  388. }
  389. };
  390. // 添加获取角色路由数据的函数
  391. const refreshRoleRouteData = async () => {
  392. try {
  393. // 使用角色路由工具更新角色路由
  394. await refreshRoleRoute();
  395. } catch (error) {
  396. console.error('获取角色路由数据失败:', error);
  397. }
  398. };
  399. export {
  400. createLoginData,
  401. createVerificationCodeLogic,
  402. getTenantId,
  403. loginLogic,
  404. loadLoginData,
  405. checkLoginStatus,
  406. generateRules,
  407. autoLogin,
  408. logoutLogic,
  409. removeLocalStorageKey
  410. }