hooks_sys.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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.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. // console.log('token状态变化,当前token:', token);
  105. // if (token) {
  106. // // 从localStorage中恢复token时设置全局请求头
  107. // axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  108. // console.log('从状态中恢复全局Authorization头:', axios.defaults.headers.common['Authorization']);
  109. // } else {
  110. // // 登出时删除请求头
  111. // delete axios.defaults.headers.common['Authorization'];
  112. // console.log('已删除全局Authorization头');
  113. // }
  114. // 设置响应拦截器处理401错误
  115. const responseInterceptor = axios.interceptors.response.use(
  116. (response) => response,
  117. (error) => {
  118. if (error.response?.status === 401) {
  119. console.log('检测到401错误,执行登出操作');
  120. handleLogout();
  121. }
  122. return Promise.reject(error);
  123. }
  124. );
  125. // 清理拦截器
  126. return () => {
  127. axios.interceptors.response.eject(responseInterceptor);
  128. };
  129. }, [token]);
  130. const handleLogin = async (username: string, password: string): Promise<void> => {
  131. try {
  132. // 使用AuthAPI登录
  133. const response = await AuthAPI.login(username, password);
  134. // 保存token和用户信息
  135. const { token: newToken, user: newUser } = response;
  136. // 设置全局默认请求头
  137. axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
  138. // 保存状态
  139. setToken(newToken);
  140. setUser(newUser);
  141. setIsAuthenticated(true);
  142. localStorage.setItem('token', newToken);
  143. } catch (error) {
  144. console.error('登录失败:', error);
  145. throw error;
  146. }
  147. };
  148. return (
  149. <AuthContext.Provider
  150. value={{
  151. user,
  152. token,
  153. login: handleLogin,
  154. logout: handleLogout,
  155. isAuthenticated,
  156. isLoading
  157. }}
  158. >
  159. {children}
  160. </AuthContext.Provider>
  161. );
  162. };
  163. // 主题提供器组件
  164. export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  165. const [isDark, setIsDark] = useState(false);
  166. const [currentTheme, setCurrentTheme] = useState<ThemeSettings>({
  167. user_id: 0,
  168. theme_mode: ThemeMode.LIGHT,
  169. primary_color: '#1890ff',
  170. font_size: FontSize.MEDIUM,
  171. is_compact: CompactMode.NORMAL
  172. });
  173. // 获取主题设置
  174. const { isLoading: isThemeLoading } = useQuery({
  175. queryKey: ['theme', 'settings'],
  176. queryFn: async () => {
  177. try {
  178. const settings = await ThemeAPI.getThemeSettings();
  179. setCurrentTheme(settings);
  180. setIsDark(settings.theme_mode === ThemeMode.DARK);
  181. return settings;
  182. } catch (error) {
  183. console.error('获取主题设置失败:', error);
  184. return null;
  185. }
  186. },
  187. refetchOnWindowFocus: false,
  188. enabled: !!localStorage.getItem('token')
  189. });
  190. // 预览主题设置(不保存到后端)
  191. const previewTheme = (newTheme: Partial<ThemeSettings>) => {
  192. const updatedTheme = { ...currentTheme, ...newTheme };
  193. setCurrentTheme(updatedTheme);
  194. setIsDark(updatedTheme.theme_mode === ThemeMode.DARK);
  195. };
  196. // 更新主题设置(保存到后端)
  197. const updateThemeMutation = useMutation({
  198. mutationFn: async (newTheme: Partial<ThemeSettings>) => {
  199. return await ThemeAPI.updateThemeSettings(newTheme);
  200. },
  201. onSuccess: (data) => {
  202. setCurrentTheme(data);
  203. setIsDark(data.theme_mode === ThemeMode.DARK);
  204. message.success('主题设置已更新');
  205. },
  206. onError: (error) => {
  207. console.error('更新主题设置失败:', error);
  208. message.error('更新主题设置失败');
  209. }
  210. });
  211. // 重置主题设置
  212. const resetThemeMutation = useMutation({
  213. mutationFn: async () => {
  214. return await ThemeAPI.resetThemeSettings();
  215. },
  216. onSuccess: (data) => {
  217. setCurrentTheme(data);
  218. setIsDark(data.theme_mode === ThemeMode.DARK);
  219. message.success('主题设置已重置为默认值');
  220. },
  221. onError: (error) => {
  222. console.error('重置主题设置失败:', error);
  223. message.error('重置主题设置失败');
  224. }
  225. });
  226. // 添加 toggleTheme 方法
  227. const toggleTheme = () => {
  228. const newTheme = {
  229. ...currentTheme,
  230. theme_mode: isDark ? ThemeMode.LIGHT : ThemeMode.DARK
  231. };
  232. setIsDark(!isDark);
  233. setCurrentTheme(newTheme);
  234. };
  235. return (
  236. <ThemeContext.Provider value={{
  237. isDark,
  238. currentTheme,
  239. updateTheme: previewTheme,
  240. saveTheme: updateThemeMutation.mutateAsync,
  241. resetTheme: resetThemeMutation.mutateAsync,
  242. toggleTheme
  243. }}>
  244. <ConfigProvider
  245. theme={{
  246. algorithm: currentTheme.is_compact === CompactMode.COMPACT
  247. ? [isDark ? theme.darkAlgorithm : theme.defaultAlgorithm, theme.compactAlgorithm]
  248. : isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
  249. token: {
  250. colorPrimary: currentTheme.primary_color,
  251. fontSize: currentTheme.font_size === FontSize.SMALL ? 12 :
  252. currentTheme.font_size === FontSize.MEDIUM ? 14 : 16,
  253. // colorBgBase: isDark ? undefined : currentTheme.background_color || '#fff',
  254. colorBgBase: currentTheme.background_color,
  255. borderRadius: currentTheme.border_radius ?? 6,
  256. colorTextBase: currentTheme.text_color || (isDark ? '#fff' : '#000'),
  257. },
  258. components: {
  259. Layout: {
  260. // headerBg: isDark ? undefined : currentTheme.background_color || '#fff',
  261. // siderBg: isDark ? undefined : currentTheme.background_color || '#fff',
  262. headerBg: currentTheme.background_color,
  263. siderBg: currentTheme.background_color,
  264. }
  265. }
  266. }}
  267. locale={locale as any}
  268. >
  269. {children}
  270. </ConfigProvider>
  271. </ThemeContext.Provider>
  272. );
  273. };
  274. // 使用上下文的钩子
  275. export const useAuth = () => {
  276. const context = useContext(AuthContext);
  277. if (!context) {
  278. throw new Error('useAuth必须在AuthProvider内部使用');
  279. }
  280. return context;
  281. };
  282. export const useTheme = () => {
  283. const context = useContext(ThemeContext);
  284. if (!context) {
  285. throw new Error('useTheme必须在ThemeProvider内部使用');
  286. }
  287. return context;
  288. };