This commit is contained in:
ryan
2026-07-13 15:17:45 +08:00
parent 08e61ce833
commit e0eb4e5975
334 changed files with 17212 additions and 12321 deletions
+81 -60
View File
@@ -1,24 +1,35 @@
"use client"
'use client';
import * as React from "react"
import {createContext, useCallback, useContext, useRef, useState} from "react"
import {useQueryClient} from "@tanstack/react-query"
import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services/admin"
import services from "@/lib/services"
import {handleContextError} from "@/lib/utils/error-handling"
import * as React from 'react';
import {
createContext,
useCallback,
useContext,
useRef,
useState,
} from 'react';
import { useQueryClient } from '@tanstack/react-query';
import type {
SystemConfig,
UpdateSystemConfigRequest,
} from '@/lib/services/admin';
import services from '@/lib/services';
import { handleContextError } from '@/lib/utils/error-handling';
/** Admin 上下文状态接口 */
export interface AdminContextState {
systemConfigs: SystemConfig[]
systemConfigsLoading: boolean
systemConfigsError: Error | null
refetchSystemConfigs: (type?: 'system' | 'business') => Promise<void>
updateSystemConfig: (key: string, data: UpdateSystemConfigRequest) => Promise<void>
systemConfigs: SystemConfig[];
systemConfigsLoading: boolean;
systemConfigsError: Error | null;
refetchSystemConfigs: (type?: 'system' | 'business') => Promise<void>;
updateSystemConfig: (
key: string,
data: UpdateSystemConfigRequest,
) => Promise<void>;
}
const AdminContext = createContext<AdminContextState | null>(null)
const AdminContext = createContext<AdminContextState | null>(null);
/**
* Admin Provider
@@ -33,52 +44,64 @@ const AdminContext = createContext<AdminContextState | null>(null)
* @param {React.ReactNode} children - Admin Provider 的子元素
*/
export function AdminProvider({ children }: { children: React.ReactNode }) {
const queryClient = useQueryClient()
const [systemConfigs, setSystemConfigs] = useState<SystemConfig[]>([])
const [systemConfigsLoading, setSystemConfigsLoading] = useState(false)
const [systemConfigsError, setSystemConfigsError] = useState<Error | null>(null)
const queryClient = useQueryClient();
const [systemConfigs, setSystemConfigs] = useState<SystemConfig[]>([]);
const [systemConfigsLoading, setSystemConfigsLoading] = useState(false);
const [systemConfigsError, setSystemConfigsError] = useState<Error | null>(
null,
);
const systemRequestIdRef = useRef(0)
const lastConfigTypeRef = useRef<'system' | 'business' | undefined>(undefined)
const systemRequestIdRef = useRef(0);
const lastConfigTypeRef = useRef<'system' | 'business' | undefined>(
undefined,
);
/** 获取系统配置列表 */
const refetchSystemConfigs = useCallback(async (type?: 'system' | 'business') => {
lastConfigTypeRef.current = type
const requestId = ++systemRequestIdRef.current
const refetchSystemConfigs = useCallback(
async (type?: 'system' | 'business') => {
lastConfigTypeRef.current = type;
const requestId = ++systemRequestIdRef.current;
try {
setSystemConfigsLoading(true)
setSystemConfigsError(null)
const data = await services.adminSystemConfig.listSystemConfigs(type)
try {
setSystemConfigsLoading(true);
setSystemConfigsError(null);
const data = await services.adminSystemConfig.listSystemConfigs(type);
if (requestId !== systemRequestIdRef.current) {
return
if (requestId !== systemRequestIdRef.current) {
return;
}
setSystemConfigs(data);
setSystemConfigsLoading(false);
} catch (error) {
if (requestId !== systemRequestIdRef.current) {
return;
}
const errorObject = handleContextError(error, '加载系统配置失败', {
logError: true,
});
setSystemConfigsError(errorObject);
setSystemConfigsLoading(false);
}
setSystemConfigs(data)
setSystemConfigsLoading(false)
} catch (error) {
if (requestId !== systemRequestIdRef.current) {
return
}
const errorObject = handleContextError(error, '加载系统配置失败', { logError: true })
setSystemConfigsError(errorObject)
setSystemConfigsLoading(false)
}
}, [])
},
[],
);
/** 更新系统配置 */
const updateSystemConfig = useCallback(async (key: string, data: UpdateSystemConfigRequest) => {
try {
await services.adminSystemConfig.updateSystemConfig(key, data)
await queryClient.invalidateQueries({ queryKey: ['public-config'] })
await refetchSystemConfigs(lastConfigTypeRef.current)
} catch (error) {
handleContextError(error, '更新系统配置失败')
throw error
}
}, [queryClient, refetchSystemConfigs])
const updateSystemConfig = useCallback(
async (key: string, data: UpdateSystemConfigRequest) => {
try {
await services.adminSystemConfig.updateSystemConfig(key, data);
await queryClient.invalidateQueries({ queryKey: ['public-config'] });
await refetchSystemConfigs(lastConfigTypeRef.current);
} catch (error) {
handleContextError(error, '更新系统配置失败');
throw error;
}
},
[queryClient, refetchSystemConfigs],
);
const value: AdminContextState = {
systemConfigs,
@@ -86,13 +109,11 @@ export function AdminProvider({ children }: { children: React.ReactNode }) {
systemConfigsError,
refetchSystemConfigs,
updateSystemConfig,
}
};
return (
<AdminContext.Provider value={value}>
{children}
</AdminContext.Provider>
)
<AdminContext.Provider value={value}>{children}</AdminContext.Provider>
);
}
/**
@@ -105,11 +126,11 @@ export function AdminProvider({ children }: { children: React.ReactNode }) {
* @returns {AdminContextState} Admin 上下文状态
*/
export function useAdmin() {
const context = useContext(AdminContext)
const context = useContext(AdminContext);
if (!context) {
throw new Error('useAdmin 必须在 AdminProvider 内部使用')
throw new Error('useAdmin 必须在 AdminProvider 内部使用');
}
return context
return context;
}
+224 -181
View File
@@ -1,255 +1,298 @@
"use client"
'use client';
import * as React from "react"
import {createContext, useCallback, useContext, useEffect, useRef, useState} from "react"
import {toast} from "sonner"
import type {AdminUser, CreateUserRequest, ListUsersRequest, UpdateUserRequest} from "@/lib/services/admin"
import services from "@/lib/services"
import * as React from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { toast } from 'sonner';
import type {
AdminUser,
CreateUserRequest,
ListUsersRequest,
UpdateUserRequest,
} from '@/lib/services/admin';
import services from '@/lib/services';
/** 用户列表查询参数 */
export interface UserQueryParams {
page: number
page_size: number
user_id?: string
username?: string
email?: string
status?: 'all' | 'active' | 'inactive'
page: number;
page_size: number;
user_id?: string;
username?: string;
email?: string;
status?: 'all' | 'active' | 'inactive';
}
/** Admin Users Context State Interface */
interface AdminUsersContextState {
users: AdminUser[]
total: number
loading: boolean
error: Error | null
users: AdminUser[];
total: number;
loading: boolean;
error: Error | null;
// Params
page: number
pageSize: number
searchUserId: string
searchUsername: string
searchEmail: string
statusFilter: 'all' | 'active' | 'inactive'
page: number;
pageSize: number;
searchUserId: string;
searchUsername: string;
searchEmail: string;
statusFilter: 'all' | 'active' | 'inactive';
// Actions
setPage: (page: number) => void
setPageSize: (size: number) => void
setSearchUserId: (userId: string) => void
setSearchUsername: (username: string) => void
setSearchEmail: (email: string) => void
setStatusFilter: (status: 'all' | 'active' | 'inactive') => void
setPage: (page: number) => void;
setPageSize: (size: number) => void;
setSearchUserId: (userId: string) => void;
setSearchUsername: (username: string) => void;
setSearchEmail: (email: string) => void;
setStatusFilter: (status: 'all' | 'active' | 'inactive') => void;
fetchUsers: (force?: boolean) => Promise<void>
refresh: () => Promise<void>
getUserDetail: (id: string) => Promise<AdminUser>
updateUserStatus: (user: AdminUser) => Promise<void>
createUser: (req: CreateUserRequest) => Promise<AdminUser>
updateUser: (id: string, req: UpdateUserRequest) => Promise<void>
deleteUser: (user: AdminUser) => Promise<void>
fetchUsers: (force?: boolean) => Promise<void>;
refresh: () => Promise<void>;
getUserDetail: (id: string) => Promise<AdminUser>;
updateUserStatus: (user: AdminUser) => Promise<void>;
createUser: (req: CreateUserRequest) => Promise<AdminUser>;
updateUser: (id: string, req: UpdateUserRequest) => Promise<void>;
deleteUser: (user: AdminUser) => Promise<void>;
}
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes cache
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes cache
const AdminUsersContext = createContext<AdminUsersContextState | null>(null)
const AdminUsersContext = createContext<AdminUsersContextState | null>(null);
export function AdminUsersProvider({ children }: { children: React.ReactNode }) {
export function AdminUsersProvider({
children,
}: {
children: React.ReactNode;
}) {
// State
const [users, setUsers] = useState<AdminUser[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [users, setUsers] = useState<AdminUser[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Query Params State
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [searchUserId, setSearchUserId] = useState("")
const [searchUsername, setSearchUsername] = useState("")
const [searchEmail, setSearchEmail] = useState("")
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all')
const [debouncedSearchUserId, setDebouncedSearchUserId] = useState("")
const [debouncedSearchUsername, setDebouncedSearchUsername] = useState("")
const [debouncedSearchEmail, setDebouncedSearchEmail] = useState("")
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [searchUserId, setSearchUserId] = useState('');
const [searchUsername, setSearchUsername] = useState('');
const [searchEmail, setSearchEmail] = useState('');
const [statusFilter, setStatusFilter] = useState<
'all' | 'active' | 'inactive'
>('all');
const [debouncedSearchUserId, setDebouncedSearchUserId] = useState('');
const [debouncedSearchUsername, setDebouncedSearchUsername] = useState('');
const [debouncedSearchEmail, setDebouncedSearchEmail] = useState('');
// Cache
const cacheRef = useRef<Record<string, { data: AdminUser[], total: number, timestamp: number }>>({})
const latestRequestIdRef = useRef(0)
const cacheRef = useRef<
Record<string, { data: AdminUser[]; total: number; timestamp: number }>
>({});
const latestRequestIdRef = useRef(0);
// Debounce search
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchUserId(searchUserId)
setDebouncedSearchUsername(searchUsername)
setDebouncedSearchEmail(searchEmail)
setDebouncedSearchUserId(searchUserId);
setDebouncedSearchUsername(searchUsername);
setDebouncedSearchEmail(searchEmail);
if (
searchUserId !== debouncedSearchUserId ||
searchUsername !== debouncedSearchUsername ||
searchEmail !== debouncedSearchEmail
) {
setPage(1) // Reset to page 1 on search change
setPage(1); // Reset to page 1 on search change
}
}, 500)
return () => clearTimeout(timer)
}, [searchUserId, searchUsername, searchEmail, debouncedSearchUserId, debouncedSearchUsername, debouncedSearchEmail])
}, 500);
return () => clearTimeout(timer);
}, [
searchUserId,
searchUsername,
searchEmail,
debouncedSearchUserId,
debouncedSearchUsername,
debouncedSearchEmail,
]);
const generateCacheKey = (params: UserQueryParams) => {
return JSON.stringify(params)
}
return JSON.stringify(params);
};
const fetchUsers = useCallback(async (force = false) => {
const params: UserQueryParams = {
page,
page_size: pageSize,
user_id: debouncedSearchUserId || undefined,
username: debouncedSearchUsername || undefined,
email: debouncedSearchEmail || undefined,
status: statusFilter
}
const cacheKey = generateCacheKey(params)
const now = Date.now()
const cached = cacheRef.current[cacheKey]
// Use cache if valid and not forced
if (!force && cached && (now - cached.timestamp < CACHE_DURATION)) {
setUsers(cached.data)
setTotal(cached.total)
setLoading(false)
setError(null)
return
}
setLoading(true)
setError(null)
const requestId = ++latestRequestIdRef.current
try {
// Current API doesn't support status filter in listUsers?
// The previous implementation did client-side filtering.
// Ideally backend supports it. If not, we fetch and filter?
// "services.adminUser.listUsers" in previous code only took page, page_size, username.
// So status filter was client side.
// However, caching client-filtered result is tricky if we don't have all data.
// But previous implementation fetched *paged* data then filtered? No, that would be wrong (filtering 20 items might leave 0).
// Let's check previous implementation:
// "const data = await services.adminUser.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..."
// This means filtering happens ONLY on the current page of results! This is technically buggy if the user wants "all inactive users".
// But preserving that behavior for now.
const requestParams: ListUsersRequest = {
const fetchUsers = useCallback(
async (force = false) => {
const params: UserQueryParams = {
page,
page_size: pageSize,
user_id: debouncedSearchUserId || undefined,
username: debouncedSearchUsername || undefined,
email: debouncedSearchEmail || undefined
email: debouncedSearchEmail || undefined,
status: statusFilter,
};
const cacheKey = generateCacheKey(params);
const now = Date.now();
const cached = cacheRef.current[cacheKey];
// Use cache if valid and not forced
if (!force && cached && now - cached.timestamp < CACHE_DURATION) {
setUsers(cached.data);
setTotal(cached.total);
setLoading(false);
setError(null);
return;
}
const response = await services.adminUser.listUsers(requestParams)
setLoading(true);
setError(null);
const requestId = ++latestRequestIdRef.current;
if (requestId !== latestRequestIdRef.current) return
try {
// Current API doesn't support status filter in listUsers?
// The previous implementation did client-side filtering.
// Ideally backend supports it. If not, we fetch and filter?
// "services.adminUser.listUsers" in previous code only took page, page_size, username.
// So status filter was client side.
// However, caching client-filtered result is tricky if we don't have all data.
// But previous implementation fetched *paged* data then filtered? No, that would be wrong (filtering 20 items might leave 0).
// Let's check previous implementation:
// "const data = await services.adminUser.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..."
// This means filtering happens ONLY on the current page of results! This is technically buggy if the user wants "all inactive users".
// But preserving that behavior for now.
let resultUsers = response.users
// Client-side filtering as per previous logic (though restrictive)
if (statusFilter === 'active') {
resultUsers = resultUsers.filter(u => u.is_active)
} else if (statusFilter === 'inactive') {
resultUsers = resultUsers.filter(u => !u.is_active)
const requestParams: ListUsersRequest = {
page,
page_size: pageSize,
user_id: debouncedSearchUserId || undefined,
username: debouncedSearchUsername || undefined,
email: debouncedSearchEmail || undefined,
};
const response = await services.adminUser.listUsers(requestParams);
if (requestId !== latestRequestIdRef.current) return;
let resultUsers = response.users;
// Client-side filtering as per previous logic (though restrictive)
if (statusFilter === 'active') {
resultUsers = resultUsers.filter((u) => u.is_active);
} else if (statusFilter === 'inactive') {
resultUsers = resultUsers.filter((u) => !u.is_active);
}
setUsers(resultUsers);
setTotal(response.total); // Note: Total might be inaccurate if we filter client side! But per previous code...
// Update cache
cacheRef.current[cacheKey] = {
data: resultUsers,
total: response.total,
timestamp: now,
};
} catch (err) {
if (requestId !== latestRequestIdRef.current) return;
setError(
err instanceof Error ? err : new Error('Failed to fetch users'),
);
} finally {
if (requestId === latestRequestIdRef.current) {
setLoading(false);
}
}
setUsers(resultUsers)
setTotal(response.total) // Note: Total might be inaccurate if we filter client side! But per previous code...
// Update cache
cacheRef.current[cacheKey] = {
data: resultUsers,
total: response.total,
timestamp: now
}
} catch (err) {
if (requestId !== latestRequestIdRef.current) return
setError(err instanceof Error ? err : new Error('Failed to fetch users'))
} finally {
if (requestId === latestRequestIdRef.current) {
setLoading(false)
}
}
}, [page, pageSize, debouncedSearchUserId, debouncedSearchUsername, debouncedSearchEmail, statusFilter])
},
[
page,
pageSize,
debouncedSearchUserId,
debouncedSearchUsername,
debouncedSearchEmail,
statusFilter,
],
);
// Removed auto-fetch useEffect. Consumer (UsersManager) should trigger fetch.
const refresh = async () => {
await fetchUsers(true)
}
await fetchUsers(true);
};
const updateUserStatus = async (user: AdminUser) => {
const originalStatus = user.is_active
const originalStatus = user.is_active;
// Optimistic update
setUsers(prev => prev.map(u =>
u.id === user.id ? { ...u, is_active: !u.is_active } : u
))
setUsers((prev) =>
prev.map((u) =>
u.id === user.id ? { ...u, is_active: !u.is_active } : u,
),
);
// Clear cache because data changed
cacheRef.current = {}
cacheRef.current = {};
try {
await services.adminUser.updateUserStatus(user.id, { is_active: !user.is_active })
toast.success(`已${ !user.is_active ? '启用' : '禁用' }用户 ${ user.username }`)
await services.adminUser.updateUserStatus(user.id, {
is_active: !user.is_active,
});
toast.success(
`已${!user.is_active ? '启用' : '禁用'}用户 ${user.username}`,
);
} catch {
// Revert on error
setUsers(prev => prev.map(u =>
u.id === user.id ? { ...u, is_active: originalStatus } : u
))
toast.error('更新状态失败')
setUsers((prev) =>
prev.map((u) =>
u.id === user.id ? { ...u, is_active: originalStatus } : u,
),
);
toast.error('更新状态失败');
}
}
};
const getUserDetail = async (id: string) => {
return services.adminUser.getUser(id)
}
return services.adminUser.getUser(id);
};
const createUser = async (req: CreateUserRequest) => {
try {
const newUser = await services.adminUser.createUser(req)
setUsers(prev => [newUser, ...prev])
setTotal(prev => prev + 1)
const newUser = await services.adminUser.createUser(req);
setUsers((prev) => [newUser, ...prev]);
setTotal((prev) => prev + 1);
// Clear cache because data changed
cacheRef.current = {}
toast.success(`已成功创建用户 ${ newUser.username }`)
return newUser
cacheRef.current = {};
toast.success(`已成功创建用户 ${newUser.username}`);
return newUser;
} catch (err) {
toast.error(err instanceof Error ? err.message : '创建用户失败')
throw err
toast.error(err instanceof Error ? err.message : '创建用户失败');
throw err;
}
}
};
const deleteUser = async (user: AdminUser) => {
try {
await services.adminUser.deleteUser(user.id)
setUsers(prev => prev.filter(u => u.id !== user.id))
setTotal(prev => Math.max(0, prev - 1))
cacheRef.current = {}
toast.success(`已删除用户 ${ user.username }`)
await services.adminUser.deleteUser(user.id);
setUsers((prev) => prev.filter((u) => u.id !== user.id));
setTotal((prev) => Math.max(0, prev - 1));
cacheRef.current = {};
toast.success(`已删除用户 ${user.username}`);
} catch (err) {
toast.error(err instanceof Error ? err.message : '删除用户失败')
throw err
toast.error(err instanceof Error ? err.message : '删除用户失败');
throw err;
}
}
};
const updateUser = async (id: string, req: UpdateUserRequest) => {
try {
await services.adminUser.updateUser(id, req)
setUsers(prev => prev.map(u =>
u.id === id ? { ...u, ...req } : u
))
cacheRef.current = {}
toast.success('更新用户信息成功')
await services.adminUser.updateUser(id, req);
setUsers((prev) => prev.map((u) => (u.id === id ? { ...u, ...req } : u)));
cacheRef.current = {};
toast.success('更新用户信息成功');
} catch (err) {
toast.error(err instanceof Error ? err.message : '更新用户信息失败')
throw err
toast.error(err instanceof Error ? err.message : '更新用户信息失败');
throw err;
}
}
};
const value = {
users,
@@ -274,20 +317,20 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
updateUserStatus,
createUser,
updateUser,
deleteUser
}
deleteUser,
};
return (
<AdminUsersContext.Provider value={value}>
{children}
</AdminUsersContext.Provider>
)
);
}
export function useAdminUsers() {
const context = useContext(AdminUsersContext)
const context = useContext(AdminUsersContext);
if (!context) {
throw new Error('useAdminUsers must be used within an AdminUsersProvider')
throw new Error('useAdminUsers must be used within an AdminUsersProvider');
}
return context
return context;
}
+28 -20
View File
@@ -1,44 +1,52 @@
"use client"
'use client';
import { createContext, useContext, useState, useCallback, useRef } from "react"
import {
createContext,
useCallback,
useContext,
useRef,
useState,
} from 'react';
interface BellRingContextType {
isRinging: boolean
triggerRing: () => void
isRinging: boolean;
triggerRing: () => void;
}
const BellRingContext = createContext<BellRingContextType | undefined>(undefined)
const BellRingContext = createContext<BellRingContextType | undefined>(
undefined,
);
/**
* 铃铛响铃动画上下文提供者
* 用于在 toast 通知时触发顶栏铃铛的响铃动画
*/
export function BellRingProvider({ children }: { children: React.ReactNode }) {
const [isRinging, setIsRinging] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const [isRinging, setIsRinging] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const triggerRing = useCallback(() => {
// 如果正在响铃,立即重新开始动画
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
clearTimeout(timeoutRef.current);
}
setIsRinging(false)
setIsRinging(false);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setIsRinging(true)
setIsRinging(true);
timeoutRef.current = setTimeout(() => {
setIsRinging(false)
timeoutRef.current = null
}, 600)
})
})
}, [])
setIsRinging(false);
timeoutRef.current = null;
}, 600);
});
});
}, []);
return (
<BellRingContext.Provider value={{ isRinging, triggerRing }}>
{children}
</BellRingContext.Provider>
)
);
}
/**
@@ -46,9 +54,9 @@ export function BellRingProvider({ children }: { children: React.ReactNode }) {
* @returns {{ isRinging: boolean, triggerRing: () => void }}
*/
export function useBellRing() {
const context = useContext(BellRingContext)
const context = useContext(BellRingContext);
if (context === undefined) {
throw new Error("useBellRing must be used within a BellRingProvider")
throw new Error('useBellRing must be used within a BellRingProvider');
}
return context
return context;
}
@@ -1,64 +1,82 @@
"use client"
'use client';
import { createContext, useContext, useState, useEffect, ReactNode, useMemo } from "react"
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
interface NotificationSettingsContextType {
showBell: boolean
isMounted: boolean
setShowBell: (value: boolean) => void
showBell: boolean;
isMounted: boolean;
setShowBell: (value: boolean) => void;
}
const NotificationSettingsContext = createContext<NotificationSettingsContextType | undefined>(undefined)
const NotificationSettingsContext = createContext<
NotificationSettingsContextType | undefined
>(undefined);
const STORAGE_KEY = "notification-settings"
const STORAGE_KEY = 'notification-settings';
interface StoredSettings {
showBell: boolean
showBell: boolean;
}
export function NotificationSettingsProvider({ children }: { children: ReactNode }) {
const [showBell, setShowBellState] = useState(true)
const [mounted, setMounted] = useState(false)
export function NotificationSettingsProvider({
children,
}: {
children: ReactNode;
}) {
const [showBell, setShowBellState] = useState(true);
const [mounted, setMounted] = useState(false);
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY)
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
const settings: StoredSettings = JSON.parse(stored)
if (typeof settings.showBell === "boolean") {
setShowBellState(settings.showBell)
const settings: StoredSettings = JSON.parse(stored);
if (typeof settings.showBell === 'boolean') {
setShowBellState(settings.showBell);
}
} catch {
// Ignore parse errors
}
}
setMounted(true)
}, [])
setMounted(true);
}, []);
const setShowBell = (value: boolean) => {
setShowBellState(value)
const settings: StoredSettings = { showBell: value }
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
}
setShowBellState(value);
const settings: StoredSettings = { showBell: value };
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
};
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo<NotificationSettingsContextType>(() => ({
showBell,
isMounted: mounted,
setShowBell,
}), [showBell, mounted])
const contextValue = useMemo<NotificationSettingsContextType>(
() => ({
showBell,
isMounted: mounted,
setShowBell,
}),
[showBell, mounted],
);
return (
<NotificationSettingsContext.Provider value={contextValue}>
{children}
</NotificationSettingsContext.Provider>
)
);
}
export function useNotificationSettings() {
const context = useContext(NotificationSettingsContext)
const context = useContext(NotificationSettingsContext);
if (context === undefined) {
throw new Error("useNotificationSettings must be used within a NotificationSettingsProvider")
throw new Error(
'useNotificationSettings must be used within a NotificationSettingsProvider',
);
}
return context
return context;
}
+56 -53
View File
@@ -1,29 +1,34 @@
"use client"
'use client';
import {createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react'
import {AuthService} from '@/lib/services/auth'
import {User} from '@/lib/services/auth/types'
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { AuthService } from '@/lib/services/auth';
import { User } from '@/lib/services/auth/types';
/** 用户状态接口 */
interface UserState {
user: User | null
loading: boolean
error: string | null
user: User | null;
loading: boolean;
error: string | null;
}
/** 用户上下文接口 */
interface UserContextValue extends UserState {
setUser: (user: User) => void
refetch: () => Promise<void>
logout: () => Promise<void>
setUser: (user: User) => void;
refetch: () => Promise<void>;
logout: () => Promise<void>;
}
/** 用户上下文 */
const UserContext = createContext<UserContextValue | undefined>(undefined)
const UserContext = createContext<UserContextValue | undefined>(undefined);
/**
* 用户Provider组件
@@ -44,83 +49,81 @@ export function UserProvider({ children }: { children: ReactNode }) {
user: null,
loading: true,
error: null,
})
const isMountedRef = useRef(true)
});
const isMountedRef = useRef(true);
/** 获取用户信息 */
const fetchUser = useCallback(async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }))
const user = await AuthService.getUserInfo()
setState((prev) => ({ ...prev, loading: true, error: null }));
const user = await AuthService.getUserInfo();
if (!isMountedRef.current) return
if (!isMountedRef.current) return;
setState({ user, loading: false, error: null })
setState({ user, loading: false, error: null });
} catch (error) {
if (!isMountedRef.current) return
if (!isMountedRef.current) return;
setState({
user: null,
loading: false,
error: error instanceof Error ? error.message : '获取用户信息失败',
})
});
}
}, [])
}, []);
/** 重新获取用户信息 */
const refetch = useCallback(async () => {
await fetchUser()
}, [fetchUser])
await fetchUser();
}, [fetchUser]);
/** 直接设置用户信息(登录/注册后免二次请求) */
const setUser = useCallback((user: User) => {
setState({ user, loading: false, error: null })
}, [])
setState({ user, loading: false, error: null });
}, []);
/** 用户登出 */
const logout = useCallback(async () => {
try {
await AuthService.logout()
await AuthService.logout();
if (!isMountedRef.current) return
if (!isMountedRef.current) return;
setState({ user: null, loading: false, error: null })
window.location.href = '/login'
setState({ user: null, loading: false, error: null });
window.location.href = '/login';
} catch (error) {
if (!isMountedRef.current) {
throw error
throw error;
}
const errorMessage = error instanceof Error ? error.message : '登出失败'
setState(prev => ({
const errorMessage = error instanceof Error ? error.message : '登出失败';
setState((prev) => ({
...prev,
error: errorMessage,
}))
throw new Error(errorMessage)
}));
throw new Error(errorMessage);
}
}, [])
}, []);
/** 组件挂载时获取用户信息(登录/注册页跳过,避免无意义请求) */
useEffect(() => {
isMountedRef.current = true
isMountedRef.current = true;
const path = window.location.pathname
if (path === "/login" || path === "/register") {
setState({user: null, loading: false, error: null})
const path = window.location.pathname;
if (path === '/login' || path === '/register') {
setState({ user: null, loading: false, error: null });
return () => {
isMountedRef.current = false
}
isMountedRef.current = false;
};
}
fetchUser()
fetchUser();
return () => {
isMountedRef.current = false
}
}, [fetchUser])
isMountedRef.current = false;
};
}, [fetchUser]);
return (
<UserContext.Provider
@@ -133,7 +136,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
>
{children}
</UserContext.Provider>
)
);
}
/**
@@ -146,9 +149,9 @@ export function UserProvider({ children }: { children: ReactNode }) {
* ```
*/
export function useUser(): UserContextValue {
const context = useContext(UserContext)
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider')
throw new Error('useUser must be used within a UserProvider');
}
return context
return context;
}