loginUtils.js 14 KB

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