2
0

hooks.tsx 7.5 KB

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