mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { createContext, useContext, useState, useRef, useCallback } from "react"
|
||||
|
||||
import services from "@/lib/services"
|
||||
import type { UpdateUserPayConfigRequest } from "@/lib/services/admin/types"
|
||||
import type { UserPayConfig, SystemConfig, UpdateSystemConfigRequest } from "@/lib/services"
|
||||
import { handleContextError } from "@/lib/utils/error-handling"
|
||||
|
||||
|
||||
/** Admin 上下文状态接口 */
|
||||
export interface AdminContextState {
|
||||
userPayConfigs: UserPayConfig[]
|
||||
userPayConfigsLoading: boolean
|
||||
userPayConfigsError: Error | null
|
||||
refetchUserPayConfigs: () => Promise<void>
|
||||
updateUserPayConfig: (id: string, data: UpdateUserPayConfigRequest) => Promise<void>
|
||||
deleteUserPayConfig: (id: string) => Promise<void>
|
||||
|
||||
systemConfigs: SystemConfig[]
|
||||
systemConfigsLoading: boolean
|
||||
systemConfigsError: Error | null
|
||||
refetchSystemConfigs: () => Promise<void>
|
||||
updateSystemConfig: (key: string, data: UpdateSystemConfigRequest) => Promise<void>
|
||||
deleteSystemConfig: (key: string) => Promise<void>
|
||||
}
|
||||
|
||||
const AdminContext = createContext<AdminContextState | null>(null)
|
||||
|
||||
/**
|
||||
* Admin Provider
|
||||
* 提供 admin 相关的数据状态管理
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AdminProvider>
|
||||
* <div>内容</div>
|
||||
* </AdminProvider>
|
||||
* ```
|
||||
* @param {React.ReactNode} children - Admin Provider 的子元素
|
||||
*/
|
||||
export function AdminProvider({ children }: { children: React.ReactNode }) {
|
||||
const [userPayConfigs, setUserPayConfigs] = useState<UserPayConfig[]>([])
|
||||
const [userPayConfigsLoading, setUserPayConfigsLoading] = useState(false)
|
||||
const [userPayConfigsError, setUserPayConfigsError] = useState<Error | null>(null)
|
||||
|
||||
const [systemConfigs, setSystemConfigs] = useState<SystemConfig[]>([])
|
||||
const [systemConfigsLoading, setSystemConfigsLoading] = useState(false)
|
||||
const [systemConfigsError, setSystemConfigsError] = useState<Error | null>(null)
|
||||
|
||||
const userPayRequestIdRef = useRef(0)
|
||||
const systemRequestIdRef = useRef(0)
|
||||
|
||||
/** 获取用户积分配置列表 */
|
||||
const refetchUserPayConfigs = useCallback(async () => {
|
||||
const requestId = ++userPayRequestIdRef.current
|
||||
|
||||
try {
|
||||
setUserPayConfigsLoading(true)
|
||||
setUserPayConfigsError(null)
|
||||
const data = await services.admin.listUserPayConfigs()
|
||||
|
||||
if (requestId !== userPayRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
setUserPayConfigs(data)
|
||||
setUserPayConfigsLoading(false)
|
||||
} catch (error) {
|
||||
if (requestId !== userPayRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const errorObject = handleContextError(error, '加载积分配置失败', { logError: true })
|
||||
setUserPayConfigsError(errorObject)
|
||||
setUserPayConfigsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
/** 更新用户积分配置 */
|
||||
const updateUserPayConfig = useCallback(async (id: string, data: UpdateUserPayConfigRequest) => {
|
||||
try {
|
||||
await services.admin.updateUserPayConfig(id, data)
|
||||
await refetchUserPayConfigs()
|
||||
} catch (error) {
|
||||
handleContextError(error, '更新用户积分配置失败')
|
||||
throw error
|
||||
}
|
||||
}, [refetchUserPayConfigs])
|
||||
|
||||
/** 删除用户积分配置 */
|
||||
const deleteUserPayConfig = useCallback(async (id: string) => {
|
||||
await services.admin.deleteUserPayConfig(id)
|
||||
await refetchUserPayConfigs()
|
||||
}, [refetchUserPayConfigs])
|
||||
|
||||
/** 获取系统配置列表 */
|
||||
const refetchSystemConfigs = useCallback(async () => {
|
||||
const requestId = ++systemRequestIdRef.current
|
||||
|
||||
try {
|
||||
setSystemConfigsLoading(true)
|
||||
setSystemConfigsError(null)
|
||||
const data = await services.admin.listSystemConfigs()
|
||||
|
||||
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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 更新系统配置 */
|
||||
const updateSystemConfig = useCallback(async (key: string, data: UpdateSystemConfigRequest) => {
|
||||
try {
|
||||
await services.admin.updateSystemConfig(key, data)
|
||||
await refetchSystemConfigs()
|
||||
} catch (error) {
|
||||
handleContextError(error, '更新系统配置失败')
|
||||
throw error
|
||||
}
|
||||
}, [refetchSystemConfigs])
|
||||
|
||||
/** 删除系统配置 */
|
||||
const deleteSystemConfig = useCallback(async (key: string) => {
|
||||
await services.admin.deleteSystemConfig(key)
|
||||
await refetchSystemConfigs()
|
||||
}, [refetchSystemConfigs])
|
||||
|
||||
const value: AdminContextState = {
|
||||
userPayConfigs,
|
||||
userPayConfigsLoading,
|
||||
userPayConfigsError,
|
||||
refetchUserPayConfigs,
|
||||
updateUserPayConfig,
|
||||
deleteUserPayConfig,
|
||||
|
||||
systemConfigs,
|
||||
systemConfigsLoading,
|
||||
systemConfigsError,
|
||||
refetchSystemConfigs,
|
||||
updateSystemConfig,
|
||||
deleteSystemConfig,
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminContext.Provider value={value}>
|
||||
{children}
|
||||
</AdminContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Admin 上下文
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { userPayConfigs, systemConfigs } = useAdmin()
|
||||
* ```
|
||||
* @returns {AdminContextState} Admin 上下文状态
|
||||
*/
|
||||
export function useAdmin() {
|
||||
const context = useContext(AdminContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAdmin 必须在 AdminProvider 内部使用')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { createContext, useContext, useCallback, useState, useRef, useEffect } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { AdminService, AdminUser, ListUsersRequest } from "@/lib/services"
|
||||
|
||||
/** 用户列表查询参数 */
|
||||
export interface UserQueryParams {
|
||||
page: number
|
||||
page_size: number
|
||||
user_id?: string
|
||||
username?: string
|
||||
status?: 'all' | 'active' | 'inactive'
|
||||
}
|
||||
|
||||
/** Admin Users Context State Interface */
|
||||
interface AdminUsersContextState {
|
||||
users: AdminUser[]
|
||||
total: number
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
|
||||
// Params
|
||||
page: number
|
||||
pageSize: number
|
||||
searchUserId: string
|
||||
searchUsername: string
|
||||
statusFilter: 'all' | 'active' | 'inactive'
|
||||
|
||||
// Actions
|
||||
setPage: (page: number) => void
|
||||
setPageSize: (size: number) => void
|
||||
setSearchUserId: (userId: string) => void
|
||||
setSearchUsername: (username: string) => void
|
||||
setStatusFilter: (status: 'all' | 'active' | 'inactive') => void
|
||||
|
||||
fetchUsers: (force?: boolean) => Promise<void>
|
||||
refresh: () => Promise<void>
|
||||
updateUserStatus: (user: AdminUser) => Promise<void>
|
||||
}
|
||||
|
||||
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes cache
|
||||
|
||||
const AdminUsersContext = createContext<AdminUsersContextState | null>(null)
|
||||
|
||||
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)
|
||||
|
||||
// Query Params State
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [searchUserId, setSearchUserId] = useState("")
|
||||
const [searchUsername, setSearchUsername] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all')
|
||||
const [debouncedSearchUserId, setDebouncedSearchUserId] = useState("")
|
||||
const [debouncedSearchUsername, setDebouncedSearchUsername] = useState("")
|
||||
|
||||
// Cache
|
||||
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)
|
||||
if (searchUserId !== debouncedSearchUserId || searchUsername !== debouncedSearchUsername) {
|
||||
setPage(1) // Reset to page 1 on search change
|
||||
}
|
||||
}, 500)
|
||||
return () => clearTimeout(timer)
|
||||
}, [searchUserId, searchUsername, debouncedSearchUserId, debouncedSearchUsername])
|
||||
|
||||
const generateCacheKey = (params: UserQueryParams) => {
|
||||
return JSON.stringify(params)
|
||||
}
|
||||
|
||||
const fetchUsers = useCallback(async (force = false) => {
|
||||
const params: UserQueryParams = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
user_id: debouncedSearchUserId || undefined,
|
||||
username: debouncedSearchUsername || 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?
|
||||
// "AdminService.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 AdminService.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 = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
user_id: debouncedSearchUserId || undefined,
|
||||
username: debouncedSearchUsername || undefined
|
||||
}
|
||||
|
||||
const response = await AdminService.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)
|
||||
}
|
||||
}
|
||||
}, [page, pageSize, debouncedSearchUserId, debouncedSearchUsername, statusFilter])
|
||||
|
||||
// Removed auto-fetch useEffect. Consumer (UsersManager) should trigger fetch.
|
||||
|
||||
|
||||
const refresh = async () => {
|
||||
await fetchUsers(true)
|
||||
}
|
||||
|
||||
const updateUserStatus = async (user: AdminUser) => {
|
||||
const originalStatus = user.is_active
|
||||
|
||||
// Optimistic update
|
||||
setUsers(prev => prev.map(u =>
|
||||
u.id === user.id ? { ...u, is_active: !u.is_active } : u
|
||||
))
|
||||
|
||||
// Clear cache because data changed
|
||||
cacheRef.current = {}
|
||||
|
||||
try {
|
||||
await AdminService.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('更新状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
const value = {
|
||||
users,
|
||||
total,
|
||||
loading,
|
||||
error,
|
||||
page,
|
||||
pageSize,
|
||||
searchUserId,
|
||||
searchUsername,
|
||||
statusFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
setSearchUserId,
|
||||
setSearchUsername,
|
||||
setStatusFilter,
|
||||
fetchUsers,
|
||||
refresh,
|
||||
updateUserStatus
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminUsersContext.Provider value={value}>
|
||||
{children}
|
||||
</AdminUsersContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAdminUsers() {
|
||||
const context = useContext(AdminUsersContext)
|
||||
if (!context) {
|
||||
throw new Error('useAdminUsers must be used within an AdminUsersProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useState, useCallback, useRef } from "react"
|
||||
|
||||
interface BellRingContextType {
|
||||
isRinging: boolean
|
||||
triggerRing: () => void
|
||||
}
|
||||
|
||||
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 triggerRing = useCallback(() => {
|
||||
// 如果正在响铃,立即重新开始动画
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
setIsRinging(false)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setIsRinging(true)
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setIsRinging(false)
|
||||
timeoutRef.current = null
|
||||
}, 600)
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BellRingContext.Provider value={{ isRinging, triggerRing }}>
|
||||
{children}
|
||||
</BellRingContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用铃铛响铃动画的 Hook
|
||||
* @returns {{ isRinging: boolean, triggerRing: () => void }}
|
||||
*/
|
||||
export function useBellRing() {
|
||||
const context = useContext(BellRingContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useBellRing must be used within a BellRingProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useState, useRef, useCallback, useEffect } from "react"
|
||||
|
||||
import services, { type MerchantAPIKey, type UpdateAPIKeyRequest } from "@/lib/services"
|
||||
import { handleContextError } from "@/lib/utils/error-handling"
|
||||
|
||||
|
||||
/** 商户数据状态接口 */
|
||||
interface MerchantDataState {
|
||||
apiKeys: MerchantAPIKey[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
/** 商户 Context 接口 */
|
||||
interface MerchantContextType extends MerchantDataState {
|
||||
loadAPIKeys: () => Promise<void>
|
||||
createAPIKey: (data: {
|
||||
app_name: string
|
||||
app_homepage_url: string
|
||||
redirect_uri?: string
|
||||
notify_url: string
|
||||
}) => Promise<MerchantAPIKey>
|
||||
updateAPIKey: (id: string, data: UpdateAPIKeyRequest) => Promise<void>
|
||||
deleteAPIKey: (id: string) => Promise<void>
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
/** 商户 Context(全局状态管理) */
|
||||
const MerchantContext = createContext<MerchantContextType | undefined>(undefined)
|
||||
|
||||
/** 商户 Provider */
|
||||
export function MerchantProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState<MerchantDataState>({
|
||||
apiKeys: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
/** 使用 ref 来标记是否已经加载过数据(全局级别) */
|
||||
const hasLoadedRef = useRef(false)
|
||||
const isMountedRef = useRef(true)
|
||||
const apiKeysRef = useRef(state.apiKeys)
|
||||
|
||||
/** 同步更新 ref */
|
||||
apiKeysRef.current = state.apiKeys
|
||||
|
||||
/** 获取 API Keys */
|
||||
const loadAPIKeys = useCallback(async () => {
|
||||
if (hasLoadedRef.current && apiKeysRef.current.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }))
|
||||
|
||||
const data = await services.merchant.listAPIKeys()
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
const validKeys = Array.isArray(data) ? data.filter(key => key != null) : []
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
apiKeys: validKeys,
|
||||
loading: false,
|
||||
error: null
|
||||
}))
|
||||
|
||||
hasLoadedRef.current = true
|
||||
} catch (error) {
|
||||
const errorObject = handleContextError(error, '加载 API Keys 失败', { showToast: true })
|
||||
if (errorObject.message === '请求已取消') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: errorObject.message
|
||||
}))
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 创建 API Key */
|
||||
const createAPIKey = useCallback(async (data: {
|
||||
app_name: string
|
||||
app_homepage_url: string
|
||||
redirect_uri?: string
|
||||
notify_url: string
|
||||
}): Promise<MerchantAPIKey> => {
|
||||
const newKey = await services.merchant.createAPIKey(data)
|
||||
|
||||
if (!isMountedRef.current) return newKey
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
apiKeys: [newKey, ...prev.apiKeys]
|
||||
}))
|
||||
|
||||
return newKey
|
||||
}, [])
|
||||
|
||||
/** 更新 API Key */
|
||||
const updateAPIKey = useCallback(async (id: string, data: UpdateAPIKeyRequest): Promise<void> => {
|
||||
await services.merchant.updateAPIKey(id, data)
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
/** 更新本地状态中的API Key */
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
apiKeys: prev.apiKeys.map(key =>
|
||||
key.id === id ? { ...key, ...data } : key
|
||||
)
|
||||
}))
|
||||
}, [])
|
||||
|
||||
/** 删除 API Key */
|
||||
const deleteAPIKey = useCallback(async (id: string) => {
|
||||
await services.merchant.deleteAPIKey(id)
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
apiKeys: prev.apiKeys.filter(key => key && key.id !== id)
|
||||
}))
|
||||
}, [])
|
||||
|
||||
/** 刷新数据 */
|
||||
const refresh = useCallback(async () => {
|
||||
hasLoadedRef.current = false
|
||||
await loadAPIKeys()
|
||||
}, [loadAPIKeys])
|
||||
|
||||
const value: MerchantContextType = {
|
||||
...state,
|
||||
loadAPIKeys,
|
||||
createAPIKey,
|
||||
updateAPIKey,
|
||||
deleteAPIKey,
|
||||
refresh,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MerchantContext.Provider value={value}>
|
||||
{children}
|
||||
</MerchantContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用商户数据 Hook
|
||||
* 用于在组件中使用商户数据状态
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { apiKeys, loading, error, loadAPIKeys, createAPIKey, updateAPIKey, deleteAPIKey, refresh } = useMerchant()
|
||||
* ```
|
||||
*/
|
||||
export function useMerchant(): MerchantContextType {
|
||||
const context = useContext(MerchantContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useMerchant must be used within a MerchantProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode, useMemo } from "react"
|
||||
|
||||
interface NotificationSettingsContextType {
|
||||
showBell: boolean
|
||||
isMounted: boolean
|
||||
setShowBell: (value: boolean) => void
|
||||
}
|
||||
|
||||
const NotificationSettingsContext = createContext<NotificationSettingsContextType | undefined>(undefined)
|
||||
|
||||
const STORAGE_KEY = "notification-settings"
|
||||
|
||||
interface StoredSettings {
|
||||
showBell: boolean
|
||||
}
|
||||
|
||||
export function NotificationSettingsProvider({ children }: { children: ReactNode }) {
|
||||
const [showBell, setShowBellState] = useState(true)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
const settings: StoredSettings = JSON.parse(stored)
|
||||
if (typeof settings.showBell === "boolean") {
|
||||
setShowBellState(settings.showBell)
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
const setShowBell = (value: boolean) => {
|
||||
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])
|
||||
|
||||
return (
|
||||
<NotificationSettingsContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</NotificationSettingsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useNotificationSettings() {
|
||||
const context = useContext(NotificationSettingsContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useNotificationSettings must be used within a NotificationSettingsProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { createContext, useContext, useCallback, useState, useRef, useEffect } from "react"
|
||||
|
||||
import services from "@/lib/services"
|
||||
import type { Order, TransactionQueryParams } from "@/lib/services"
|
||||
import { generateTransactionCacheKey } from "@/lib/utils"
|
||||
|
||||
|
||||
/** 交易上下文状态接口 */
|
||||
interface TransactionContextState {
|
||||
transactions: Order[]
|
||||
total: number
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
lastParams: Partial<TransactionQueryParams>
|
||||
fetchTransactions: (params: Partial<TransactionQueryParams>) => Promise<void>
|
||||
goToPage: (page: number) => Promise<void>
|
||||
setPageSize: (size: number) => void
|
||||
refresh: () => Promise<void>
|
||||
clearCacheAndRefresh: () => Promise<void>
|
||||
reset: () => void
|
||||
updateOrderStatus: (orderId: string, updates: Partial<Pick<Order, 'status' | 'dispute_id'>>) => void
|
||||
}
|
||||
|
||||
const MAX_CACHE_SIZE = 50
|
||||
const CACHE_DURATION = 5 * 60 * 1000 // 5分钟
|
||||
|
||||
/** 交易上下文 */
|
||||
const TransactionContext = createContext<TransactionContextState | null>(null)
|
||||
|
||||
/** 交易 Provider Props 接口 */
|
||||
interface TransactionProviderProps {
|
||||
children: React.ReactNode
|
||||
defaultParams?: Partial<TransactionQueryParams>
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易 Provider
|
||||
* 提供交易数据的全局状态管理
|
||||
*
|
||||
* @param {React.ReactNode} children - 交易 Provider 的子元素
|
||||
* @param {Partial<TransactionQueryParams>} defaultParams - 默认查询参数
|
||||
* @example
|
||||
* ```tsx
|
||||
* <TransactionProvider defaultParams={{ type: 'receive', page_size: 20 }}>
|
||||
* <TransactionList />
|
||||
* </TransactionProvider>
|
||||
* ```
|
||||
*/
|
||||
export function TransactionProvider({ children, defaultParams = {} }: TransactionProviderProps) {
|
||||
const [transactions, setTransactions] = useState<Order[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(defaultParams.page_size || 20)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [lastParams, setLastParams] = useState<Partial<TransactionQueryParams>>(defaultParams)
|
||||
|
||||
/** 使用 useRef 存储缓存 */
|
||||
const cacheRef = useRef<Record<string, { data: Order[], total: number, timestamp: number }>>({})
|
||||
const latestRequestIdRef = useRef(0)
|
||||
|
||||
const cleanExpiredCache = useCallback(() => {
|
||||
const now = Date.now()
|
||||
Object.keys(cacheRef.current).forEach(key => {
|
||||
if (now - cacheRef.current[key].timestamp > CACHE_DURATION) {
|
||||
delete cacheRef.current[key]
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const addToCache = useCallback((key: string, value: { data: Order[], total: number, timestamp: number }) => {
|
||||
const cacheKeys = Object.keys(cacheRef.current)
|
||||
|
||||
if (cacheKeys.length >= MAX_CACHE_SIZE) {
|
||||
const oldestKey = cacheKeys.reduce((oldest, current) => {
|
||||
return cacheRef.current[current].timestamp < cacheRef.current[oldest].timestamp ? current : oldest
|
||||
})
|
||||
delete cacheRef.current[oldestKey]
|
||||
}
|
||||
|
||||
cacheRef.current[key] = value
|
||||
}, [])
|
||||
|
||||
/** 获取交易列表 */
|
||||
const fetchTransactions = useCallback(async (params: Partial<TransactionQueryParams>) => {
|
||||
const queryParams: TransactionQueryParams = {
|
||||
page: params.page || 1,
|
||||
page_size: params.page_size || pageSize,
|
||||
...params,
|
||||
}
|
||||
|
||||
/** 生成唯一的请求 ID */
|
||||
const requestId = ++latestRequestIdRef.current
|
||||
|
||||
/** 生成缓存key */
|
||||
const cacheKey = generateTransactionCacheKey(queryParams as Parameters<typeof generateTransactionCacheKey>[0])
|
||||
|
||||
const cached = cacheRef.current[cacheKey]
|
||||
const now = Date.now()
|
||||
|
||||
/** 缓存所有页面,不仅仅第一页 */
|
||||
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
||||
if (requestId !== latestRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
/** 使用缓存数据,同步更新状态 */
|
||||
setTransactions(cached.data)
|
||||
setTotal(cached.total)
|
||||
setCurrentPage(queryParams.page)
|
||||
setPageSize(queryParams.page_size)
|
||||
setLastParams(prev => ({ ...prev, ...params }))
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
/** 发起API请求 */
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
// 不清空 total,保持显示之前的总数
|
||||
// setTransactions([]) - 也不清空,保持之前的数据直到新数据到达
|
||||
|
||||
try {
|
||||
const result = await services.transaction.getTransactions(queryParams)
|
||||
|
||||
if (requestId !== latestRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
/** 总是替换数据,不累加 */
|
||||
setTransactions(result.orders)
|
||||
|
||||
/** 缓存所有页的数据 */
|
||||
addToCache(cacheKey, {
|
||||
data: result.orders,
|
||||
total: result.total,
|
||||
timestamp: now
|
||||
})
|
||||
cleanExpiredCache()
|
||||
|
||||
setTotal(result.total)
|
||||
setCurrentPage(queryParams.page)
|
||||
setPageSize(queryParams.page_size)
|
||||
setLastParams(prev => ({ ...prev, ...params }))
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message === '请求已被取消') {
|
||||
return
|
||||
}
|
||||
|
||||
if (requestId !== latestRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
setError(err instanceof Error ? err : new Error('获取交易记录失败'))
|
||||
console.error('获取交易记录失败:', err)
|
||||
} finally {
|
||||
if (requestId === latestRequestIdRef.current) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [pageSize, addToCache, cleanExpiredCache])
|
||||
|
||||
/** 跳转到指定页 */
|
||||
const goToPage = useCallback(async (page: number) => {
|
||||
if (loading) return
|
||||
await fetchTransactions({
|
||||
...lastParams,
|
||||
page,
|
||||
})
|
||||
}, [fetchTransactions, lastParams, loading])
|
||||
|
||||
/** 设置分页大小 */
|
||||
const setPageSizeHandler = useCallback((size: number) => {
|
||||
setPageSize(size)
|
||||
fetchTransactions({
|
||||
...lastParams,
|
||||
page: 1,
|
||||
page_size: size,
|
||||
})
|
||||
}, [fetchTransactions, lastParams])
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const cacheKey = generateTransactionCacheKey({ ...lastParams, page: currentPage, page_size: pageSize } as Parameters<typeof generateTransactionCacheKey>[0])
|
||||
delete cacheRef.current[cacheKey]
|
||||
|
||||
await fetchTransactions({
|
||||
...lastParams,
|
||||
page: currentPage,
|
||||
})
|
||||
}, [fetchTransactions, lastParams, pageSize, currentPage])
|
||||
|
||||
/** 清除所有缓存并刷新 */
|
||||
const clearCacheAndRefresh = useCallback(async () => {
|
||||
cacheRef.current = {}
|
||||
await fetchTransactions({
|
||||
...lastParams,
|
||||
page: 1,
|
||||
})
|
||||
}, [fetchTransactions, lastParams])
|
||||
|
||||
/** 乐观更新订单状态 */
|
||||
const updateOrderStatus = useCallback((orderId: string, updates: Partial<Pick<Order, 'status' | 'dispute_id'>>) => {
|
||||
setTransactions(prev =>
|
||||
prev.map(order =>
|
||||
order.id === orderId
|
||||
? { ...order, ...updates }
|
||||
: order
|
||||
)
|
||||
)
|
||||
|
||||
cacheRef.current = {}
|
||||
}, [])
|
||||
|
||||
/** 重置状态 */
|
||||
const reset = useCallback(() => {
|
||||
setTransactions([])
|
||||
setTotal(0)
|
||||
setCurrentPage(1)
|
||||
setPageSize(defaultParams.page_size || 20)
|
||||
setError(null)
|
||||
setLastParams(defaultParams)
|
||||
}, [defaultParams])
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
|
||||
const value: TransactionContextState = {
|
||||
transactions,
|
||||
total,
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
loading,
|
||||
error,
|
||||
lastParams,
|
||||
fetchTransactions,
|
||||
goToPage,
|
||||
setPageSize: setPageSizeHandler,
|
||||
refresh,
|
||||
clearCacheAndRefresh,
|
||||
reset,
|
||||
updateOrderStatus,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cacheRef.current = {}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TransactionContext.Provider value={value}>
|
||||
{children}
|
||||
</TransactionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用交易上下文
|
||||
*
|
||||
* @param {React.ReactNode} children - 交易上下文 Provider 的子元素
|
||||
* @param {Partial<TransactionQueryParams>} defaultParams - 默认查询参数
|
||||
|
||||
*/
|
||||
export function useTransaction() {
|
||||
const context = useContext(TransactionContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useTransaction 必须在 TransactionProvider 内部使用')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useRef, useCallback, ReactNode } from 'react'
|
||||
|
||||
import services from '@/lib/services'
|
||||
import { User, TrustLevel, PayLevel } from '@/lib/services/auth/types'
|
||||
|
||||
|
||||
/** 用户状态接口 */
|
||||
interface UserState {
|
||||
user: User | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
/** 用户上下文接口 */
|
||||
interface UserContextValue extends UserState {
|
||||
refetch: () => Promise<void>
|
||||
updatePayKey: (payKey: string) => Promise<void>
|
||||
getTrustLevelLabel: (trustLevel: TrustLevel) => string
|
||||
getPayLevelLabel: (payLevel: PayLevel) => string
|
||||
logout: () => Promise<void>
|
||||
}
|
||||
|
||||
/** 信任等级映射 */
|
||||
const TRUST_LEVEL_LABELS: Record<TrustLevel, string> = {
|
||||
[TrustLevel.New]: '新用户',
|
||||
[TrustLevel.Basic]: '基本用户',
|
||||
[TrustLevel.Member]: '成员',
|
||||
[TrustLevel.Regular]: '活跃用户',
|
||||
[TrustLevel.Leader]: '领导者',
|
||||
}
|
||||
|
||||
/** 支付等级映射 */
|
||||
const PAY_LEVEL_LABELS: Record<PayLevel, string> = {
|
||||
[PayLevel.BlackGold]: '黑金',
|
||||
[PayLevel.WhiteGold]: '白金',
|
||||
[PayLevel.Gold]: '黄金',
|
||||
[PayLevel.Ordinary]: '普通',
|
||||
}
|
||||
|
||||
/** 用户上下文 */
|
||||
const UserContext = createContext<UserContextValue | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* 用户Provider组件
|
||||
*
|
||||
* @param {React.ReactNode} children - 用户 Provider 的子元素
|
||||
* @returns {React.ReactNode} 用户 Provider 组件
|
||||
* @example
|
||||
* ```tsx
|
||||
* <UserProvider>
|
||||
* <UserContext.Provider value={{ user, loading, error, refetch, updatePayKey, getTrustLevelLabel, getPayLevelLabel, logout }}>
|
||||
* {children}
|
||||
* </UserContext.Provider>
|
||||
* </UserProvider>
|
||||
* ```
|
||||
*/
|
||||
export function UserProvider({ children }: { children: ReactNode }) {
|
||||
const [state, setState] = useState<UserState>({
|
||||
user: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const isMountedRef = useRef(true)
|
||||
|
||||
/** 获取信任等级标签 */
|
||||
const getTrustLevelLabel = useCallback((trustLevel: TrustLevel): string => {
|
||||
return TRUST_LEVEL_LABELS[trustLevel] || '未知'
|
||||
}, [])
|
||||
|
||||
/** 获取支付等级标签 */
|
||||
const getPayLevelLabel = useCallback((payLevel: PayLevel): string => {
|
||||
return PAY_LEVEL_LABELS[payLevel] || '未知'
|
||||
}, [])
|
||||
|
||||
/** 获取用户信息 */
|
||||
const fetchUser = useCallback(async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }))
|
||||
const user = await services.auth.getUserInfo()
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setState({ user, loading: false, error: null })
|
||||
} catch (error) {
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setState({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : '获取用户信息失败',
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 重新获取用户信息 */
|
||||
const refetch = useCallback(async () => {
|
||||
await fetchUser()
|
||||
}, [fetchUser])
|
||||
|
||||
/** 更新支付密码 */
|
||||
const updatePayKey = useCallback(async (payKey: string) => {
|
||||
await services.user.updatePayKey(payKey)
|
||||
await fetchUser()
|
||||
}, [fetchUser])
|
||||
|
||||
/** 用户登出 */
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await services.auth.logout()
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setState({ user: null, loading: false, error: null })
|
||||
window.location.href = '/login'
|
||||
} catch (error) {
|
||||
if (!isMountedRef.current) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : '登出失败'
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: errorMessage,
|
||||
}))
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 组件挂载时获取用户信息 */
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
fetchUser()
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [fetchUser])
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
...state,
|
||||
refetch,
|
||||
updatePayKey,
|
||||
getTrustLevelLabel,
|
||||
getPayLevelLabel,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用用户上下文的Hook
|
||||
*
|
||||
* @returns {UserContextValue} 用户上下文值
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { user, loading, error, refetch, updatePayKey, getTrustLevelLabel, getPayLevelLabel, logout } = useUser()
|
||||
* ```
|
||||
*/
|
||||
export function useUser(): UserContextValue {
|
||||
const context = useContext(UserContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useUser must be used within a UserProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user