user.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import { computed, ref } from "vue";
  2. import { forInObject, isNull, isObject, parse, storage } from "../utils";
  3. import { router } from "../router";
  4. import { request } from "../service";
  5. import type { UserInfo } from "../types";
  6. import { type LoginData, getUsersInfo, refreshTokenApi } from "@/services/user";
  7. import { dict } from "./dict";
  8. export type Token = {
  9. token: string; // 访问token
  10. expire: number; // token过期时间(秒)
  11. refreshToken: string; // 刷新token
  12. refreshExpire: number; // 刷新token过期时间(秒)
  13. };
  14. export class User {
  15. /**
  16. * 用户信息,响应式对象
  17. */
  18. info = ref<UserInfo | null>(null);
  19. /**
  20. * token刷新中的Promise,防止并发刷新
  21. */
  22. private refreshing: Promise<string> | null = null;
  23. /**
  24. * 用户信息,响应式对象
  25. */
  26. courseId = ref<string | null>(null);
  27. /**
  28. * 当前token,字符串或null
  29. */
  30. token: string | 'Basic ZW5kOmVuZA==';
  31. constructor() {
  32. // 获取本地用户信息
  33. const userInfo = storage.get("userInfo");
  34. // 获取本地token
  35. const token = storage.get("token") as string | 'Basic ZW5kOmVuZA==';
  36. // 如果token为空字符串则置为null
  37. this.token = token == "" ? 'Basic ZW5kOmVuZA==' : token;
  38. if (this.token == 'Basic ZW5kOmVuZA==') {
  39. return;
  40. }
  41. // 初始化用户信息
  42. if (userInfo != null && isObject(userInfo)) {
  43. this.set(userInfo);
  44. } else {
  45. this.get()
  46. }
  47. }
  48. /**
  49. * 获取用户信息(从服务端拉取最新信息并更新本地)
  50. * @returns Promise<void>
  51. */
  52. async get() {
  53. try {
  54. const res = await getUsersInfo()
  55. if (res != null) {
  56. this.set(res);
  57. }
  58. } catch (err) {
  59. console.warn("获取用户信息失败:", err);
  60. }
  61. }
  62. /**
  63. * 设置用户信息并存储到本地
  64. * @param data 用户信息对象
  65. */
  66. set(data: any) {
  67. if (isNull(data)) {
  68. return;
  69. }
  70. // 设置
  71. this.info.value = parse<UserInfo>(data)!;
  72. // 持久化到本地存储
  73. storage.set("userInfo", data, 0);
  74. }
  75. setCourseId(id: string) {
  76. this.courseId.value = id
  77. }
  78. /**
  79. * 更新用户信息(本地与服务端同步)
  80. * @param data 新的用户信息
  81. */
  82. async update(data: any) {
  83. if (isNull(data) || isNull(this.info.value)) {
  84. return;
  85. }
  86. // 本地同步更新
  87. forInObject(data, (value, key) => {
  88. this.info.value![key] = value;
  89. });
  90. // 同步到服务端
  91. await request({
  92. url: "/app/user/info/updatePerson",
  93. method: "POST",
  94. data
  95. });
  96. }
  97. /**
  98. * 移除用户信息
  99. */
  100. remove() {
  101. this.info.value = null;
  102. storage.remove("userInfo");
  103. }
  104. /**
  105. * 判断用户信息是否为空
  106. * @returns boolean
  107. */
  108. isNull() {
  109. return this.info.value == null;
  110. }
  111. /**
  112. * 清除本地所有用户信息和token
  113. */
  114. clear() {
  115. storage.remove("userInfo");
  116. storage.remove("token");
  117. storage.remove("refreshToken");
  118. this.token = 'Basic ZW5kOmVuZA==';
  119. this.remove();
  120. this.info.value = null;
  121. this.courseId.value = null;
  122. }
  123. /**
  124. * 退出登录,清除所有信息并跳转到登录页
  125. */
  126. logout() {
  127. this.clear();
  128. router.login();
  129. }
  130. /**
  131. * 设置token并存储到本地
  132. * @param data Token对象
  133. */
  134. setToken(data: LoginData) {
  135. this.token = `Bearer ${data?.access_token}`;
  136. // 访问token,提前5秒过期,防止边界问题
  137. storage.set("token", this.token, 7 * 24 * 60 * 60 - 5);
  138. this.get();
  139. dict.refresh();
  140. // 存储refresh_token(服务端默认30天有效期)
  141. if (data != null && data.refresh_token != null) {
  142. storage.set("refreshToken", data.refresh_token, 30 * 24 * 60 * 60 - 5);
  143. }
  144. }
  145. /**
  146. * 刷新token(调用服务端OAuth2接口,自动更新本地token)
  147. * @returns Promise<string> 新的token
  148. */
  149. async refreshToken(): Promise<string> {
  150. // 防止并发刷新
  151. if (this.refreshing != null) {
  152. return this.refreshing;
  153. }
  154. const storedRefreshToken = storage.get("refreshToken") as string;
  155. if (storedRefreshToken == '' || storedRefreshToken.length == 0) {
  156. return Promise.reject(new Error("No refresh token available"));
  157. }
  158. this.refreshing = (async () => {
  159. try {
  160. const res = await refreshTokenApi(storedRefreshToken);
  161. if (res != null && res.data != null) {
  162. const respData = res.data as UTSJSONObject;
  163. const loginData: LoginData = {
  164. access_token: respData["access_token"] as string,
  165. refresh_token: respData["refresh_token"] as string,
  166. };
  167. this.setToken(loginData);
  168. return this.token;
  169. }
  170. throw new Error("Refresh token response invalid");
  171. } finally {
  172. this.refreshing = null;
  173. }
  174. })();
  175. return this.refreshing;
  176. }
  177. }
  178. /**
  179. * 单例用户对象,项目全局唯一
  180. */
  181. export const user = new User();
  182. /**
  183. * 用户信息,响应式对象
  184. */
  185. export const userInfo = computed(() => user.info.value);