hooks_sys.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import React, { useState, useEffect, createContext, useContext } from 'react';
  2. import { ConfigProvider, theme, message
  3. } from 'antd';
  4. import zhCN from "antd/locale/zh_CN";
  5. import {
  6. useQuery,
  7. useQueryClient,
  8. useMutation
  9. } from '@tanstack/react-query';
  10. import axios from 'axios';
  11. import dayjs from 'dayjs';
  12. import weekday from 'dayjs/plugin/weekday';
  13. import localeData from 'dayjs/plugin/localeData';
  14. import 'dayjs/locale/zh-cn';
  15. import type {
  16. User, AuthContextType, ThemeContextType, ThemeSettings
  17. } from '../share/types.ts';
  18. import {
  19. ThemeMode,
  20. FontSize,
  21. CompactMode
  22. } from '../share/types.ts';
  23. import {
  24. AuthAPI,
  25. ThemeAPI
  26. } from './api/index.ts';
  27. // 配置 dayjs 插件
  28. dayjs.extend(weekday);
  29. dayjs.extend(localeData);
  30. // 设置 dayjs 语言
  31. dayjs.locale('zh-cn');
  32. // 确保ConfigProvider能够正确使用中文日期
  33. const locale = {
  34. ...zhCN,
  35. DatePicker: {
  36. ...zhCN.DatePicker,
  37. lang: {
  38. ...zhCN.DatePicker?.lang,
  39. shortWeekDays: ['日', '一', '二', '三', '四', '五', '六'],
  40. shortMonths: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  41. }
  42. }
  43. };
  44. // 创建认证上下文
  45. const AuthContext = createContext<AuthContextType | null>(null);
  46. const ThemeContext = createContext<ThemeContextType | null>(null);
  47. // 认证提供器组件
  48. export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  49. const [user, setUser] = useState<User | null>(null);
  50. const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
  51. const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
  52. const queryClient = useQueryClient();
  53. // 声明handleLogout函数
  54. const handleLogout = async () => {
  55. try {
  56. // 如果已登录,调用登出API
  57. if (token) {
  58. await AuthAPI.logout();
  59. }
  60. } catch (error) {
  61. console.error('登出请求失败:', error);
  62. } finally {
  63. // 清除本地状态
  64. setToken(null);
  65. setUser(null);
  66. setIsAuthenticated(false);
  67. localStorage.removeItem('token');
  68. // 清除Authorization头
  69. delete axios.defaults.headers.common['Authorization'];
  70. console.log('登出时已删除全局Authorization头');
  71. // 清除所有查询缓存
  72. queryClient.clear();
  73. }
  74. };
  75. // 使用useQuery检查登录状态
  76. const { isLoading } = useQuery({
  77. queryKey: ['auth', 'status', token],
  78. queryFn: async () => {
  79. if (!token) {
  80. setIsAuthenticated(false);
  81. setUser(null);
  82. return null;
  83. }
  84. try {
  85. // 设置全局默认请求头
  86. axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  87. // 使用API验证当前用户
  88. const currentUser = await AuthAPI.getCurrentUser();
  89. setUser(currentUser);
  90. setIsAuthenticated(true);
  91. return { isValid: true, user: currentUser };
  92. } catch (error) {
  93. // 如果API调用失败,自动登出
  94. handleLogout();
  95. return { isValid: false };
  96. }
  97. },
  98. enabled: !!token,
  99. refetchOnWindowFocus: false,
  100. retry: false
  101. });
  102. // 设置请求拦截器
  103. useEffect(() => {
  104. // 设置响应拦截器处理401错误
  105. const responseInterceptor = axios.interceptors.response.use(
  106. (response) => response,
  107. (error) => {
  108. if (error.response?.status === 401) {
  109. console.log('检测到401错误,执行登出操作');
  110. handleLogout();
  111. }
  112. return Promise.reject(error);
  113. }
  114. );
  115. // 清理拦截器
  116. return () => {
  117. axios.interceptors.response.eject(responseInterceptor);
  118. };
  119. }, [token]);
  120. const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise<void> => {
  121. try {
  122. // 使用AuthAPI登录
  123. const response = await AuthAPI.login(username, password, latitude, longitude);
  124. // 保存token和用户信息
  125. const { token: newToken, user: newUser } = response;
  126. // 设置全局默认请求头
  127. axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
  128. // 保存状态
  129. setToken(newToken);
  130. setUser(newUser);
  131. setIsAuthenticated(true);
  132. localStorage.setItem('token', newToken);
  133. } catch (error) {
  134. console.error('登录失败:', error);
  135. throw error;
  136. }
  137. };
  138. return (
  139. <AuthContext.Provider
  140. value={{
  141. user,
  142. token,
  143. login: handleLogin,
  144. logout: handleLogout,
  145. isAuthenticated,
  146. isLoading
  147. }}
  148. >
  149. {children}
  150. </AuthContext.Provider>
  151. );
  152. };
  153. // 主题提供器组件
  154. export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  155. const [isDark, setIsDark] = useState(false);
  156. const [currentTheme, setCurrentTheme] = useState<ThemeSettings>({
  157. user_id: 0,
  158. theme_mode: ThemeMode.LIGHT,
  159. primary_color: '#1890ff',
  160. font_size: FontSize.MEDIUM,
  161. is_compact: CompactMode.NORMAL
  162. });
  163. // 获取主题设置
  164. const { isLoading: isThemeLoading } = useQuery({
  165. queryKey: ['theme', 'settings'],
  166. queryFn: async () => {
  167. try {
  168. const settings = await ThemeAPI.getThemeSettings();
  169. setCurrentTheme(settings);
  170. setIsDark(settings.theme_mode === ThemeMode.DARK);
  171. return settings;
  172. } catch (error) {
  173. console.error('获取主题设置失败:', error);
  174. return null;
  175. }
  176. },
  177. refetchOnWindowFocus: false,
  178. enabled: !!localStorage.getItem('token')
  179. });
  180. // 预览主题设置(不保存到后端)
  181. const previewTheme = (newTheme: Partial<ThemeSettings>) => {
  182. const updatedTheme = { ...currentTheme, ...newTheme };
  183. setCurrentTheme(updatedTheme);
  184. setIsDark(updatedTheme.theme_mode === ThemeMode.DARK);
  185. };
  186. // 更新主题设置(保存到后端)
  187. const updateThemeMutation = useMutation({
  188. mutationFn: async (newTheme: Partial<ThemeSettings>) => {
  189. return await ThemeAPI.updateThemeSettings(newTheme);
  190. },
  191. onSuccess: (data) => {
  192. setCurrentTheme(data);
  193. setIsDark(data.theme_mode === ThemeMode.DARK);
  194. message.success('主题设置已更新');
  195. },
  196. onError: (error) => {
  197. console.error('更新主题设置失败:', error);
  198. message.error('更新主题设置失败');
  199. }
  200. });
  201. // 重置主题设置
  202. const resetThemeMutation = useMutation({
  203. mutationFn: async () => {
  204. return await ThemeAPI.resetThemeSettings();
  205. },
  206. onSuccess: (data) => {
  207. setCurrentTheme(data);
  208. setIsDark(data.theme_mode === ThemeMode.DARK);
  209. message.success('主题设置已重置为默认值');
  210. },
  211. onError: (error) => {
  212. console.error('重置主题设置失败:', error);
  213. message.error('重置主题设置失败');
  214. }
  215. });
  216. // 添加 toggleTheme 方法
  217. const toggleTheme = () => {
  218. const newTheme = {
  219. ...currentTheme,
  220. theme_mode: isDark ? ThemeMode.LIGHT : ThemeMode.DARK
  221. };
  222. setIsDark(!isDark);
  223. setCurrentTheme(newTheme);
  224. };
  225. return (
  226. <ThemeContext.Provider value={{
  227. isDark,
  228. currentTheme,
  229. updateTheme: previewTheme,
  230. saveTheme: updateThemeMutation.mutateAsync,
  231. resetTheme: resetThemeMutation.mutateAsync,
  232. toggleTheme
  233. }}>
  234. <ConfigProvider
  235. theme={{
  236. algorithm: currentTheme.is_compact === CompactMode.COMPACT
  237. ? [isDark ? theme.darkAlgorithm : theme.defaultAlgorithm, theme.compactAlgorithm]
  238. : isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
  239. token: {
  240. colorPrimary: currentTheme.primary_color,
  241. fontSize: currentTheme.font_size === FontSize.SMALL ? 12 :
  242. currentTheme.font_size === FontSize.MEDIUM ? 14 : 16,
  243. // colorBgBase: isDark ? undefined : currentTheme.background_color || '#fff',
  244. colorBgBase: currentTheme.background_color,
  245. borderRadius: currentTheme.border_radius ?? 6,
  246. colorTextBase: currentTheme.text_color || (isDark ? '#fff' : '#000'),
  247. },
  248. components: {
  249. Layout: {
  250. // headerBg: isDark ? undefined : currentTheme.background_color || '#fff',
  251. // siderBg: isDark ? undefined : currentTheme.background_color || '#fff',
  252. headerBg: currentTheme.background_color,
  253. siderBg: currentTheme.background_color,
  254. }
  255. }
  256. }}
  257. locale={locale as any}
  258. >
  259. {children}
  260. </ConfigProvider>
  261. </ThemeContext.Provider>
  262. );
  263. };
  264. // 使用上下文的钩子
  265. export const useAuth = () => {
  266. const context = useContext(AuthContext);
  267. if (!context) {
  268. throw new Error('useAuth必须在AuthProvider内部使用');
  269. }
  270. return context;
  271. };
  272. export const useTheme = () => {
  273. const context = useContext(ThemeContext);
  274. if (!context) {
  275. throw new Error('useTheme必须在ThemeProvider内部使用');
  276. }
  277. return context;
  278. };