mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface CommonControlledStateProps<T> {
|
||||
value?: T;
|
||||
defaultValue?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 受控状态 Hook
|
||||
* 用于处理受控和非受控组件的状态管理
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const [value, setValue] = useControlledState({ value: propValue, onChange: console.log })
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useControlledState<T, Rest extends any[] = []>(
|
||||
props: CommonControlledStateProps<T> & {
|
||||
onChange?: (value: T, ...args: Rest) => void;
|
||||
},
|
||||
): readonly [T, (next: T, ...args: Rest) => void] {
|
||||
const { value, defaultValue, onChange } = props;
|
||||
|
||||
const [state, setInternalState] = React.useState<T>(
|
||||
value !== undefined ? value : (defaultValue as T),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value !== undefined) setInternalState(value);
|
||||
}, [value]);
|
||||
|
||||
const setState = React.useCallback(
|
||||
(next: T, ...args: Rest) => {
|
||||
setInternalState(next);
|
||||
onChange?.(next, ...args);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return [state, setState] as const;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import type { DisputeWithOrder, DisputeStatus } from "@/lib/services"
|
||||
import { useUser } from "@/contexts/user-context"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const DISPUTE_PAGE_SIZE = 20
|
||||
|
||||
/**
|
||||
* 争议数据获取 Hook
|
||||
* 用于获取争议数据
|
||||
*
|
||||
* @param fetchFn 获取争议数据的函数
|
||||
* @returns 争议数据
|
||||
*/
|
||||
interface UseDisputeDataProps {
|
||||
fetchFn: (params: { page: number; page_size: number; status: DisputeStatus }) => Promise<{ total: number; disputes: DisputeWithOrder[] }>
|
||||
}
|
||||
|
||||
export const useDisputeData = ({ fetchFn }: UseDisputeDataProps) => {
|
||||
const { user } = useUser()
|
||||
const [disputes, setDisputes] = React.useState<{ count: number; list: DisputeWithOrder[] }>({ count: 0, list: [] })
|
||||
const [loading, setLoading] = React.useState<boolean>(true)
|
||||
const fetchFnRef = React.useRef(fetchFn)
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchFnRef.current = fetchFn
|
||||
}, [fetchFn])
|
||||
|
||||
const fetchData = React.useCallback(async () => {
|
||||
if (!user) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await fetchFnRef.current({
|
||||
page: 1,
|
||||
page_size: DISPUTE_PAGE_SIZE,
|
||||
status: 'disputing'
|
||||
}).catch(() => ({ total: 0, disputes: [] }))
|
||||
|
||||
setDisputes({
|
||||
count: result.total,
|
||||
list: result.disputes
|
||||
})
|
||||
} catch (err) {
|
||||
toast.error('获取争议数据失败', {
|
||||
description: err instanceof Error ? err.message : '获取争议数据失败'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [user])
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData()
|
||||
}, [fetchData])
|
||||
|
||||
const handleRefresh = React.useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await fetchFnRef.current({
|
||||
page: 1,
|
||||
page_size: DISPUTE_PAGE_SIZE,
|
||||
status: 'disputing'
|
||||
})
|
||||
setDisputes({
|
||||
count: result.total,
|
||||
list: result.disputes
|
||||
})
|
||||
} catch (err) {
|
||||
toast.error('刷新争议数据失败', {
|
||||
description: err instanceof Error ? err.message : '刷新争议数据失败'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { disputes, loading, handleRefresh, refetchData: fetchData }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { useInView, type UseInViewOptions } from 'motion/react';
|
||||
|
||||
interface UseIsInViewOptions {
|
||||
inView?: boolean;
|
||||
inViewOnce?: boolean;
|
||||
inViewMargin?: UseInViewOptions['margin'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测元素是否在视图中
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { ref, isInView } = useIsInView({ inViewOnce: true })
|
||||
* ```
|
||||
*/
|
||||
function useIsInView<T extends HTMLElement = HTMLElement>(
|
||||
ref: React.Ref<T>,
|
||||
options: UseIsInViewOptions = {},
|
||||
) {
|
||||
const { inView, inViewOnce = false, inViewMargin = '0px' } = options;
|
||||
const localRef = React.useRef<T>(null);
|
||||
React.useImperativeHandle(ref, () => localRef.current as T);
|
||||
const inViewResult = useInView(localRef, {
|
||||
once: inViewOnce,
|
||||
margin: inViewMargin,
|
||||
});
|
||||
const isInView = !inView || inViewResult;
|
||||
return { ref: localRef, isInView };
|
||||
}
|
||||
|
||||
export { useIsInView, type UseIsInViewOptions };
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { LeaderboardService } from "@/lib/services/leaderboard";
|
||||
import type {
|
||||
LeaderboardListResponse,
|
||||
LeaderboardListRequest,
|
||||
LeaderboardEntry,
|
||||
UserRankResponse,
|
||||
} from "@/lib/services/leaderboard";
|
||||
|
||||
export const useLeaderboard = () => {
|
||||
const [data, setData] = React.useState<LeaderboardListResponse | null>(null);
|
||||
const [allItems, setAllItems] = React.useState<LeaderboardEntry[]>([]);
|
||||
const [myRank, setMyRank] = React.useState<UserRankResponse | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [loadingMore, setLoadingMore] = React.useState(false);
|
||||
const [myRankLoading, setMyRankLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<Error | null>(null);
|
||||
|
||||
const currentPageRef = React.useRef(1);
|
||||
|
||||
const fetchData = React.useCallback(
|
||||
async (
|
||||
queryParams: LeaderboardListRequest,
|
||||
append = false,
|
||||
ignore?: { current: boolean },
|
||||
) => {
|
||||
if (append) {
|
||||
setLoadingMore(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
setAllItems([]);
|
||||
currentPageRef.current = 1;
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await LeaderboardService.getList(queryParams);
|
||||
|
||||
if (ignore?.current) return;
|
||||
|
||||
setData(response);
|
||||
if (append) {
|
||||
setAllItems((prev) => [...prev, ...response.items]);
|
||||
} else {
|
||||
setAllItems(response.items);
|
||||
}
|
||||
currentPageRef.current = response.page;
|
||||
} catch (err) {
|
||||
if (ignore?.current) return;
|
||||
if (err instanceof Error) {
|
||||
setError(err);
|
||||
}
|
||||
} finally {
|
||||
if (!ignore?.current) {
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchMyRank = React.useCallback(
|
||||
async (ignore?: { current: boolean }) => {
|
||||
setMyRankLoading(true);
|
||||
try {
|
||||
const response = await LeaderboardService.getMyRank();
|
||||
if (!ignore?.current) {
|
||||
setMyRank(response);
|
||||
}
|
||||
} catch {
|
||||
if (!ignore?.current) {
|
||||
setMyRank(null);
|
||||
}
|
||||
} finally {
|
||||
if (!ignore?.current) {
|
||||
setMyRankLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const ignore = { current: false };
|
||||
fetchData({ page: 1, page_size: 50 }, false, ignore);
|
||||
fetchMyRank(ignore);
|
||||
return () => {
|
||||
ignore.current = true;
|
||||
};
|
||||
}, [fetchData, fetchMyRank]);
|
||||
|
||||
const loadNextPage = React.useCallback(() => {
|
||||
if (
|
||||
data &&
|
||||
currentPageRef.current * data.page_size < data.total &&
|
||||
!loadingMore
|
||||
) {
|
||||
const nextPage = currentPageRef.current + 1;
|
||||
fetchData({ page: nextPage, page_size: data.page_size }, true);
|
||||
}
|
||||
}, [data, loadingMore, fetchData]);
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
fetchData({ page: 1, page_size: 50 }, false);
|
||||
fetchMyRank();
|
||||
}, [fetchData, fetchMyRank]);
|
||||
|
||||
const hasMore = data
|
||||
? currentPageRef.current * data.page_size < data.total
|
||||
: false;
|
||||
|
||||
return {
|
||||
data,
|
||||
items: allItems,
|
||||
myRank,
|
||||
loading,
|
||||
loadingMore,
|
||||
myRankLoading,
|
||||
error,
|
||||
hasMore,
|
||||
loadNextPage,
|
||||
refresh,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ConfigService, PublicConfigResponse } from '@/lib/services'
|
||||
|
||||
/* 全局缓存 */
|
||||
let cachedConfig: PublicConfigResponse | null = null
|
||||
let cachePromise: Promise<PublicConfigResponse> | null = null
|
||||
let cacheTime = 0
|
||||
const CACHE_DURATION = 5 * 60 * 1000
|
||||
|
||||
/**
|
||||
* 获取公共配置(带缓存)
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { config, loading, error } = usePublicConfig()
|
||||
* ```
|
||||
*/
|
||||
export function usePublicConfig() {
|
||||
const [config, setConfig] = useState<PublicConfigResponse | null>(cachedConfig)
|
||||
const [loading, setLoading] = useState(!cachedConfig)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
const fetchConfig = async () => {
|
||||
const now = Date.now()
|
||||
|
||||
if (cachedConfig && (now - cacheTime) < CACHE_DURATION) {
|
||||
if (mounted) {
|
||||
setConfig(cachedConfig)
|
||||
setLoading(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!cachePromise) {
|
||||
cachePromise = ConfigService.getPublicConfig()
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await cachePromise
|
||||
if (mounted) {
|
||||
cachedConfig = data
|
||||
cacheTime = now
|
||||
setConfig(data)
|
||||
setError(null)
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) {
|
||||
setError(err instanceof Error ? err : new Error('获取配置失败'))
|
||||
}
|
||||
} finally {
|
||||
cachePromise = null
|
||||
if (mounted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchConfig()
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { config, loading, error }
|
||||
}
|
||||
Reference in New Issue
Block a user