hooks.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import React, { createContext, useContext, useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. import { useQuery, useQueryClient } from '@tanstack/react-query';
  4. import { getLocalStorageWithExpiry, setLocalStorageWithExpiry } from './utils.ts';
  5. import type { User, AuthContextType, ThemeContextType, ThemeSettings } from '../share/types.ts';
  6. import { ThemeMode, FontSize, CompactMode } from '../share/types.ts';
  7. import { AuthAPI, ThemeAPI } from './api.ts';
  8. // 创建axios实例
  9. const api = axios.create({
  10. baseURL: window.CONFIG?.API_BASE_URL || '/api',
  11. timeout: 10000,
  12. headers: {
  13. 'Content-Type': 'application/json',
  14. }
  15. });
  16. // 请求拦截器添加token
  17. api.interceptors.request.use(
  18. (config) => {
  19. const token = getLocalStorageWithExpiry('token');
  20. if (token) {
  21. config.headers['Authorization'] = `Bearer ${token}`;
  22. }
  23. return config;
  24. },
  25. (error) => {
  26. return Promise.reject(error);
  27. }
  28. );
  29. // 响应拦截器处理错误
  30. api.interceptors.response.use(
  31. (response) => {
  32. return response;
  33. },
  34. (error) => {
  35. if (error.response && error.response.status === 401) {
  36. // 清除本地存储并刷新页面
  37. localStorage.removeItem('token');
  38. localStorage.removeItem('user');
  39. window.location.href = '/mobile/login';
  40. }
  41. return Promise.reject(error);
  42. }
  43. );
  44. // 默认主题设置
  45. const defaultThemeSettings: ThemeSettings = {
  46. user_id: 0,
  47. theme_mode: ThemeMode.LIGHT,
  48. primary_color: '#3B82F6', // 蓝色
  49. background_color: '#F9FAFB',
  50. text_color: '#111827',
  51. border_radius: 8,
  52. font_size: FontSize.MEDIUM,
  53. is_compact: CompactMode.NORMAL
  54. };
  55. // 创建认证上下文
  56. const AuthContext = createContext<AuthContextType | null>(null);
  57. // 创建主题上下文
  58. const ThemeContext = createContext<ThemeContextType | null>(null);
  59. // 认证提供者组件
  60. export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  61. const [user, setUser] = useState<User | null>(null);
  62. const [token, setToken] = useState<string | null>(getLocalStorageWithExpiry('token'));
  63. const [isAuthenticated, setIsAuthenticated] = useState(false);
  64. const queryClient = useQueryClient();
  65. // 使用useQuery检查登录状态
  66. const { isLoading: isAuthChecking } = useQuery({
  67. queryKey: ['auth', 'status', token],
  68. queryFn: async () => {
  69. if (!token) {
  70. setUser(null);
  71. setIsAuthenticated(false);
  72. return null;
  73. }
  74. try {
  75. // 设置请求头
  76. api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  77. // 获取当前用户信息
  78. const currentUser = await AuthAPI.getCurrentUser();
  79. setUser(currentUser);
  80. setIsAuthenticated(true);
  81. setLocalStorageWithExpiry('user', currentUser, 24);
  82. return { isValid: true, user: currentUser };
  83. } catch (error) {
  84. // 如果API调用失败,自动登出
  85. logout();
  86. return { isValid: false };
  87. }
  88. },
  89. enabled: !!token,
  90. refetchOnWindowFocus: false,
  91. retry: false,
  92. });
  93. // 登录函数
  94. const login = async (username: string, password: string) => {
  95. try {
  96. const response = await AuthAPI.login(username, password);
  97. const { token, user } = response;
  98. // 保存到状态和本地存储
  99. setToken(token);
  100. setUser(user);
  101. setLocalStorageWithExpiry('token', token, 24); // 24小时过期
  102. setLocalStorageWithExpiry('user', user, 24);
  103. // 设置请求头
  104. api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  105. } catch (error) {
  106. console.error('登录失败:', error);
  107. throw error;
  108. }
  109. };
  110. // 登出函数
  111. const logout = async () => {
  112. try {
  113. // 调用登出API
  114. await AuthAPI.logout();
  115. } catch (error) {
  116. console.error('登出API调用失败:', error);
  117. } finally {
  118. // 无论API调用成功与否,都清除本地状态
  119. setToken(null);
  120. setUser(null);
  121. localStorage.removeItem('token');
  122. localStorage.removeItem('user');
  123. // 清除请求头
  124. delete api.defaults.headers.common['Authorization'];
  125. // 清除所有查询缓存
  126. queryClient.clear();
  127. }
  128. };
  129. return (
  130. <AuthContext.Provider
  131. value={{
  132. user,
  133. token,
  134. login,
  135. logout,
  136. isAuthenticated,
  137. isLoading: isAuthChecking
  138. }}
  139. >
  140. {children}
  141. </AuthContext.Provider>
  142. );
  143. };
  144. // 主题提供者组件
  145. export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  146. const [currentTheme, setCurrentTheme] = useState<ThemeSettings>(() => {
  147. const storedTheme = localStorage.getItem('theme');
  148. return storedTheme ? JSON.parse(storedTheme) : defaultThemeSettings;
  149. });
  150. const isDark = currentTheme.theme_mode === ThemeMode.DARK;
  151. // 更新主题(实时预览)
  152. const updateTheme = (theme: Partial<ThemeSettings>) => {
  153. setCurrentTheme(prev => {
  154. const updatedTheme = { ...prev, ...theme };
  155. localStorage.setItem('theme', JSON.stringify(updatedTheme));
  156. return updatedTheme;
  157. });
  158. };
  159. // 保存主题到后端
  160. const saveTheme = async (theme: Partial<ThemeSettings>): Promise<ThemeSettings> => {
  161. try {
  162. const updatedTheme = { ...currentTheme, ...theme };
  163. const data = await ThemeAPI.updateThemeSettings(updatedTheme);
  164. setCurrentTheme(data);
  165. localStorage.setItem('theme', JSON.stringify(data));
  166. return data;
  167. } catch (error) {
  168. console.error('保存主题失败:', error);
  169. throw error;
  170. }
  171. };
  172. // 重置主题
  173. const resetTheme = async (): Promise<ThemeSettings> => {
  174. try {
  175. const data = await ThemeAPI.resetThemeSettings();
  176. setCurrentTheme(data);
  177. localStorage.setItem('theme', JSON.stringify(data));
  178. return data;
  179. } catch (error) {
  180. console.error('重置主题失败:', error);
  181. // 如果API失败,至少重置到默认主题
  182. setCurrentTheme(defaultThemeSettings);
  183. localStorage.setItem('theme', JSON.stringify(defaultThemeSettings));
  184. return defaultThemeSettings;
  185. }
  186. };
  187. // 切换主题模式(亮色/暗色)
  188. const toggleTheme = () => {
  189. const newMode = isDark ? ThemeMode.LIGHT : ThemeMode.DARK;
  190. const updatedTheme = {
  191. ...currentTheme,
  192. theme_mode: newMode,
  193. // 暗色和亮色模式下自动调整背景色和文字颜色
  194. background_color: newMode === ThemeMode.DARK ? '#121212' : '#F9FAFB',
  195. text_color: newMode === ThemeMode.DARK ? '#E5E7EB' : '#111827'
  196. };
  197. setCurrentTheme(updatedTheme);
  198. localStorage.setItem('theme', JSON.stringify(updatedTheme));
  199. };
  200. // 主题变化时应用CSS变量
  201. useEffect(() => {
  202. document.documentElement.style.setProperty('--primary-color', currentTheme.primary_color);
  203. document.documentElement.style.setProperty('--background-color', currentTheme.background_color || '#F9FAFB');
  204. document.documentElement.style.setProperty('--text-color', currentTheme.text_color || '#111827');
  205. document.documentElement.style.setProperty('--border-radius', `${currentTheme.border_radius || 8}px`);
  206. // 设置字体大小
  207. let rootFontSize = '16px'; // 默认中等字体
  208. if (currentTheme.font_size === FontSize.SMALL) {
  209. rootFontSize = '14px';
  210. } else if (currentTheme.font_size === FontSize.LARGE) {
  211. rootFontSize = '18px';
  212. }
  213. document.documentElement.style.setProperty('--font-size', rootFontSize);
  214. // 设置暗色模式类
  215. if (isDark) {
  216. document.documentElement.classList.add('dark');
  217. } else {
  218. document.documentElement.classList.remove('dark');
  219. }
  220. }, [currentTheme, isDark]);
  221. return (
  222. <ThemeContext.Provider
  223. value={{
  224. isDark,
  225. currentTheme,
  226. updateTheme,
  227. saveTheme,
  228. resetTheme,
  229. toggleTheme
  230. }}
  231. >
  232. {children}
  233. </ThemeContext.Provider>
  234. );
  235. };
  236. // 使用上下文的钩子
  237. export const useAuth = () => {
  238. const context = useContext(AuthContext);
  239. if (!context) {
  240. throw new Error('useAuth必须在AuthProvider内部使用');
  241. }
  242. return context;
  243. };
  244. export const useTheme = () => {
  245. const context = useContext(ThemeContext);
  246. if (!context) {
  247. throw new Error('useTheme必须在ThemeProvider内部使用');
  248. }
  249. return context;
  250. };