loginUtils.js 12 KB

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