@@ -130,10 +96,4 @@ const RowComponent = ({
);
};
-const Row = React.memo(RowComponent);
-
-Row.displayName = "FileListRow";
-
export { RowComponent };
-
-export default Row;
diff --git a/src/components/file/utils/fileListLayout.ts b/src/components/file/utils/fileListLayout.ts
index f903071..db47a1f 100644
--- a/src/components/file/utils/fileListLayout.ts
+++ b/src/components/file/utils/fileListLayout.ts
@@ -1,4 +1,5 @@
import {
+ FILE_ITEM_CONFIG,
LIST_HEIGHT_CONFIG,
TOP_ELEMENTS_ESTIMATE,
BOTTOM_RESERVED_SPACE,
@@ -15,6 +16,42 @@ interface LayoutMetricsParams {
viewportHeight: number | null;
}
+export const getRowMetrics = (isSmallScreen: boolean): {
+ rowHeight: number;
+ rowPaddingBottom: number;
+} => {
+ const baseHeight = isSmallScreen
+ ? FILE_ITEM_CONFIG.baseHeight.xs
+ : FILE_ITEM_CONFIG.baseHeight.sm;
+
+ const rowGap = FILE_ITEM_CONFIG.spacing.marginBottom;
+
+ return {
+ rowHeight: baseHeight + rowGap,
+ rowPaddingBottom: rowGap,
+ };
+};
+
+export const getListPadding = (needsScrolling: boolean, isSmallScreen: boolean): {
+ paddingTop: number;
+ paddingBottom: number;
+} => {
+ // 非滚动模式使用对称内边距,让短列表更居中
+ if (!needsScrolling) {
+ const padding = isSmallScreen ? 16 : 20;
+ return {
+ paddingTop: padding - 4,
+ paddingBottom: padding,
+ };
+ }
+
+ // 滚动模式收紧内边距,尽可能展示更多行
+ return {
+ paddingTop: 0,
+ paddingBottom: 8,
+ };
+};
+
export const calculateLayoutMetrics = ({
fileCount,
rowHeight,
@@ -93,4 +130,3 @@ export const calculateLayoutMetrics = ({
needsScrolling: true,
};
};
-
diff --git a/src/components/file/utils/types.ts b/src/components/file/utils/types.ts
index 3c61195..d066e7d 100644
--- a/src/components/file/utils/types.ts
+++ b/src/components/file/utils/types.ts
@@ -14,10 +14,10 @@ export interface VirtualListItemData {
isScrolling: boolean;
scrollSpeed: number;
highlightedIndex: number | null;
+ rowPaddingBottom: number;
}
export interface FileListLayoutMetrics {
height: number;
needsScrolling: boolean;
}
-
diff --git a/src/components/interactions/SearchDrawer/FallbackDialog.tsx b/src/components/interactions/SearchDrawer/FallbackDialog.tsx
index 40786e3..7f2b66c 100644
--- a/src/components/interactions/SearchDrawer/FallbackDialog.tsx
+++ b/src/components/interactions/SearchDrawer/FallbackDialog.tsx
@@ -6,6 +6,7 @@ import {
DialogTitle,
Typography
} from "@mui/material";
+import React from "react";
interface FallbackDialogProps {
open: boolean;
diff --git a/src/components/interactions/SearchDrawer/FilterSection.tsx b/src/components/interactions/SearchDrawer/FilterSection.tsx
index 3f6768b..44e1231 100644
--- a/src/components/interactions/SearchDrawer/FilterSection.tsx
+++ b/src/components/interactions/SearchDrawer/FilterSection.tsx
@@ -17,6 +17,7 @@ import {
} from "@mui/icons-material";
import { g3BorderRadius, G3_PRESETS } from "@/theme/g3Curves";
import { useI18n } from "@/contexts/I18nContext";
+import React from "react";
interface FilterSectionProps {
expanded: boolean;
diff --git a/src/components/interactions/SearchDrawer/IndexStatus.tsx b/src/components/interactions/SearchDrawer/IndexStatus.tsx
index d80cf6b..bba096e 100644
--- a/src/components/interactions/SearchDrawer/IndexStatus.tsx
+++ b/src/components/interactions/SearchDrawer/IndexStatus.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from "react";
+import React, { useMemo } from "react";
import {
Alert,
Box,
@@ -13,13 +13,14 @@ import { Refresh as RefreshIcon } from "@mui/icons-material";
import { g3BorderRadius, G3_PRESETS } from "@/theme/g3Curves";
import { useI18n } from "@/contexts/I18nContext";
import type { InterpolationOptions } from "@/utils/i18n/types";
+import { SearchIndexErrorCode } from "@/services/github/core/searchIndex/errors";
const FALLBACK_INDEX_TIME = Date.now();
interface IndexStatusProps {
enabled: boolean;
loading: boolean;
- error: { message: string; code?: string } | null;
+ error: { message: string; code?: SearchIndexErrorCode } | null;
ready: boolean;
indexedBranches: string[];
lastUpdatedAt: number | undefined;
@@ -32,50 +33,52 @@ interface ErrorScenario {
}
const getErrorScenario = (
- error: { message: string; code?: string } | null,
+ error: { message: string; code?: SearchIndexErrorCode } | null,
ready: boolean,
t: (key: string, options?: InterpolationOptions) => string
): ErrorScenario | null => {
- if (error?.code !== undefined) {
- switch (error.code) {
- case 'SEARCH_INDEX_MANIFEST_NOT_FOUND':
- return {
- title: t('search.index.errors.manifestNotFound.title'),
- description: [
- t('search.index.errors.manifestNotFound.description1'),
- t('search.index.errors.manifestNotFound.description2'),
- ],
- };
- case 'SEARCH_INDEX_MANIFEST_INVALID':
- return {
- title: t('search.index.errors.manifestInvalid.title'),
- description: [t('search.index.errors.manifestInvalid.description1')],
- };
- case 'SEARCH_INDEX_FILE_NOT_FOUND':
- return {
- title: t('search.index.errors.fileNotFound.title'),
- description: [
- t('search.index.errors.fileNotFound.description1'),
- t('search.index.errors.fileNotFound.description2'),
- ],
- };
- case 'SEARCH_INDEX_DOCUMENT_INVALID':
- return {
- title: t('search.index.errors.documentInvalid.title'),
- description: [
- t('search.index.errors.documentInvalid.description1'),
- t('search.index.errors.documentInvalid.description2'),
- ],
- };
- default:
- return {
- title: t('search.index.errors.default.title'),
- description: [
- t('search.index.errors.default.description1', { message: error.message }),
- t('search.index.errors.default.description2'),
- ],
- };
+ if (error !== null) {
+ const code = error.code;
+ if (code === SearchIndexErrorCode.MANIFEST_NOT_FOUND) {
+ return {
+ title: t('search.index.errors.manifestNotFound.title'),
+ description: [
+ t('search.index.errors.manifestNotFound.description1'),
+ t('search.index.errors.manifestNotFound.description2'),
+ ],
+ };
}
+ if (code === SearchIndexErrorCode.MANIFEST_INVALID) {
+ return {
+ title: t('search.index.errors.manifestInvalid.title'),
+ description: [t('search.index.errors.manifestInvalid.description1')],
+ };
+ }
+ if (code === SearchIndexErrorCode.INDEX_FILE_NOT_FOUND) {
+ return {
+ title: t('search.index.errors.fileNotFound.title'),
+ description: [
+ t('search.index.errors.fileNotFound.description1'),
+ t('search.index.errors.fileNotFound.description2'),
+ ],
+ };
+ }
+ if (code === SearchIndexErrorCode.INDEX_DOCUMENT_INVALID) {
+ return {
+ title: t('search.index.errors.documentInvalid.title'),
+ description: [
+ t('search.index.errors.documentInvalid.description1'),
+ t('search.index.errors.documentInvalid.description2'),
+ ],
+ };
+ }
+ return {
+ title: t('search.index.errors.default.title'),
+ description: [
+ t('search.index.errors.default.description1', { message: error.message }),
+ t('search.index.errors.default.description2'),
+ ],
+ };
}
if (!ready) {
diff --git a/src/components/interactions/SearchDrawer/SearchInput.tsx b/src/components/interactions/SearchDrawer/SearchInput.tsx
index 7a52b05..016d34f 100644
--- a/src/components/interactions/SearchDrawer/SearchInput.tsx
+++ b/src/components/interactions/SearchDrawer/SearchInput.tsx
@@ -10,6 +10,7 @@ import {
import { Clear as ClearIcon } from "@mui/icons-material";
import { g3BorderRadius, G3_PRESETS } from "@/theme/g3Curves";
import { useI18n } from "@/contexts/I18nContext";
+import React from "react";
interface SearchInputProps {
value: string;
diff --git a/src/components/interactions/SearchDrawer/SearchResultItem.tsx b/src/components/interactions/SearchDrawer/SearchResultItem.tsx
index 81b28e2..d008d56 100644
--- a/src/components/interactions/SearchDrawer/SearchResultItem.tsx
+++ b/src/components/interactions/SearchDrawer/SearchResultItem.tsx
@@ -8,42 +8,63 @@ import {
Stack,
Tooltip,
Typography,
- useMediaQuery,
- useTheme
} from "@mui/material";
import { GitHub as GitHubIcon } from "@mui/icons-material";
import { g3BorderRadius, G3_PRESETS } from "@/theme/g3Curves";
import { highlightKeyword, highlightKeywords, resolveItemHtmlUrl } from "./utils";
import type { RepoSearchItem } from "@/hooks/github/useRepoSearch";
import { useI18n } from "@/contexts/I18nContext";
+import React from "react";
+import type { CSSProperties } from "react";
interface SearchResultItemProps {
item: RepoSearchItem;
keyword: string;
+ keywordLower: string;
+ highlightRegex: RegExp | null;
+ isSmallScreen: boolean;
onClick: (item: RepoSearchItem) => void;
onOpenGithub: (item: RepoSearchItem) => void;
+ style?: CSSProperties;
+ ariaAttributes?: {
+ "aria-posinset": number;
+ "aria-setsize": number;
+ role: "listitem";
+ };
}
export const SearchResultItem: React.FC
= ({
item,
keyword,
+ keywordLower,
+ highlightRegex,
+ isSmallScreen,
onClick,
- onOpenGithub
+ onOpenGithub,
+ style,
+ ariaAttributes
}) => {
- const theme = useTheme();
- const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const { t } = useI18n();
- const pathParts = highlightKeyword(item.path, keyword);
+ const pathParts = highlightKeyword(item.path, keyword, keywordLower);
const githubUrl = resolveItemHtmlUrl(item);
const snippet = ("snippet" in item && typeof (item as { snippet?: unknown }).snippet === "string")
? (item as { snippet?: string }).snippet
: undefined;
- const snippetParts = snippet !== undefined && snippet.length > 0 ? highlightKeywords(snippet, keyword) : null;
+ const snippetParts = snippet !== undefined && snippet.length > 0
+ ? highlightKeywords(snippet, keyword, highlightRegex)
+ : null;
+
+ const listItemProps = {
+ disablePadding: true,
+ alignItems: "flex-start" as const,
+ ...(style !== undefined ? { style } : {}),
+ ...(ariaAttributes ?? {})
+ };
return (
-
+
void;
+ onOpenGithub: (item: RepoSearchItem) => void;
+}
+
+const VIRTUALIZE_THRESHOLD = 30;
+
+const SearchResultRow = ({
+ ariaAttributes,
+ index,
+ style,
+ ...rowData
+}: RowComponentProps): React.ReactElement => {
+ const item = rowData.items[index];
+
+ if (item === undefined) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
export const SearchResults: React.FC = ({
items,
keyword,
@@ -40,6 +91,33 @@ export const SearchResults: React.FC = ({
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const { t } = useI18n();
+ const listHeight = isSmallScreen ? 300 : 400;
+ const shouldVirtualize = items.length >= VIRTUALIZE_THRESHOLD;
+ const keywordLower = useMemo(() => keyword.toLowerCase(), [keyword]);
+ const highlightRegex = useMemo(() => getHighlightRegex(keyword), [keyword]);
+ const defaultRowHeight = isSmallScreen ? 72 : 88;
+ const dynamicRowHeight = useDynamicRowHeight({ defaultRowHeight, key: keyword });
+
+ const rowData = useMemo(
+ (): SearchResultRowData => ({
+ items,
+ keyword,
+ keywordLower,
+ highlightRegex,
+ isSmallScreen,
+ onResultClick,
+ onOpenGithub
+ }),
+ [
+ items,
+ keyword,
+ keywordLower,
+ highlightRegex,
+ isSmallScreen,
+ onResultClick,
+ onOpenGithub
+ ]
+ );
const showEmptyIndexResult =
!loading &&
@@ -81,40 +159,74 @@ export const SearchResults: React.FC = ({
)}
-
- {items.map(item => (
- 0 ? (
+
+ {
+ if (width === undefined || height === undefined) {
+ return null;
+ }
+
+ return (
+
+ );
+ }}
/>
- ))}
- {searchResult !== null && searchResult.items.length === 0 && (
-
-
+ ) : (
+
+ {items.map(item => (
+
-
- )}
-
+ ))}
+ {searchResult !== null && searchResult.items.length === 0 && (
+
+
+
+ )}
+
+ )}
>
);
};
diff --git a/src/components/interactions/SearchDrawer/index.tsx b/src/components/interactions/SearchDrawer/index.tsx
index 994c8b7..dd2438f 100644
--- a/src/components/interactions/SearchDrawer/index.tsx
+++ b/src/components/interactions/SearchDrawer/index.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useMemo } from "react";
+import React, { useCallback, useMemo } from "react";
import {
Alert,
Button,
diff --git a/src/components/interactions/SearchDrawer/utils.ts b/src/components/interactions/SearchDrawer/utils.ts
index 4b67215..54f6a0f 100644
--- a/src/components/interactions/SearchDrawer/utils.ts
+++ b/src/components/interactions/SearchDrawer/utils.ts
@@ -22,8 +22,9 @@ export const parseExtensionInput = (value: string): string[] => {
* 高亮文本中的关键字
*/
export const highlightKeyword = (
- text: string,
- keyword: string
+ text: string,
+ keyword: string,
+ lowerKeywordOverride?: string
): { text: string; highlight: boolean }[] => {
if (keyword.trim().length === 0) {
return [{ text, highlight: false }];
@@ -31,7 +32,7 @@ export const highlightKeyword = (
const parts: { text: string; highlight: boolean }[] = [];
const lowerText = text.toLowerCase();
- const lowerKeyword = keyword.toLowerCase();
+ const lowerKeyword = lowerKeywordOverride ?? keyword.toLowerCase();
let lastIndex = 0;
let index = lowerText.indexOf(lowerKeyword);
@@ -59,20 +60,18 @@ export const highlightKeyword = (
const escapeRegExp = (value: string): string =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-/**
- * 高亮文本中的多个关键字
- */
-export const highlightKeywords = (
- text: string,
- keyword: string
-): { text: string; highlight: boolean }[] => {
+const HIGHLIGHT_REGEX_CACHE_LIMIT = 50;
+// 关键词高亮正则缓存,减少重复编译
+const highlightRegexCache = new Map();
+
+export const getHighlightRegex = (keyword: string): RegExp | null => {
const tokens = keyword
.split(/\s+/)
.map((token) => token.trim())
.filter((token) => token.length > 0);
if (tokens.length === 0) {
- return [{ text, highlight: false }];
+ return null;
}
const uniqueTokens = Array.from(new Set(tokens))
@@ -81,10 +80,42 @@ export const highlightKeywords = (
const pattern = uniqueTokens.join("|");
if (pattern.length === 0) {
- return [{ text, highlight: false }];
+ return null;
+ }
+
+ const cached = highlightRegexCache.get(pattern);
+ if (cached !== undefined) {
+ return cached;
}
const regex = new RegExp(`(${pattern})`, "gi");
+ highlightRegexCache.set(pattern, regex);
+
+ if (highlightRegexCache.size > HIGHLIGHT_REGEX_CACHE_LIMIT) {
+ // 简单淘汰最早插入项,避免缓存无限增长
+ const firstKey = highlightRegexCache.keys().next().value;
+ if (typeof firstKey === "string") {
+ highlightRegexCache.delete(firstKey);
+ }
+ }
+
+ return regex;
+};
+
+/**
+ * 高亮文本中的多个关键字
+ */
+export const highlightKeywords = (
+ text: string,
+ keyword: string,
+ regexOverride?: RegExp | null
+): { text: string; highlight: boolean }[] => {
+ const regex = regexOverride ?? getHighlightRegex(keyword);
+ if (regex === null) {
+ return [{ text, highlight: false }];
+ }
+
+ regex.lastIndex = 0;
const parts: { text: string; highlight: boolean }[] = [];
let lastIndex = 0;
let match = regex.exec(text);
diff --git a/src/components/interactions/index.ts b/src/components/interactions/index.ts
index 2e8708f..cb0ff5c 100644
--- a/src/components/interactions/index.ts
+++ b/src/components/interactions/index.ts
@@ -1,2 +1 @@
-export { default as ScrollToTopFab } from './ScrollToTopFab';
-export { default as SearchDrawer } from './SearchDrawer';
+export {};
diff --git a/src/components/layout/ReadmeSection.tsx b/src/components/layout/ReadmeSection.tsx
index 614be21..c6839b2 100644
--- a/src/components/layout/ReadmeSection.tsx
+++ b/src/components/layout/ReadmeSection.tsx
@@ -97,7 +97,22 @@ const ReadmeSection: React.FC = ({
const handleInternalLinkClick = useCallback(
(relativePath: string) => {
// 解析相对路径
- let targetPath = relativePath;
+ let targetPath = relativePath.trim();
+ if (targetPath.length === 0) {
+ return;
+ }
+
+ const hashIndex = targetPath.indexOf('#');
+ if (hashIndex >= 0) {
+ targetPath = targetPath.slice(0, hashIndex);
+ }
+
+ const queryIndex = targetPath.indexOf('?');
+ if (queryIndex >= 0) {
+ targetPath = targetPath.slice(0, queryIndex);
+ }
+
+ const isAbsolutePath = targetPath.startsWith('/');
// 移除开头的 ./
if (targetPath.startsWith('./')) {
@@ -105,7 +120,14 @@ const ReadmeSection: React.FC = ({
}
// 处理 ../ 路径
- const baseParts = currentReadmeDir.length > 0 ? currentReadmeDir.split('/') : [];
+ const baseParts = isAbsolutePath
+ ? []
+ : currentReadmeDir.length > 0
+ ? currentReadmeDir.split('/')
+ : [];
+ if (isAbsolutePath) {
+ targetPath = targetPath.substring(1);
+ }
const targetParts = targetPath.split('/');
const resolvedParts = [...baseParts];
diff --git a/src/components/layout/ToolbarButtons.tsx b/src/components/layout/ToolbarButtons.tsx
index fa567a3..4625523 100644
--- a/src/components/layout/ToolbarButtons.tsx
+++ b/src/components/layout/ToolbarButtons.tsx
@@ -1,4 +1,4 @@
-import { useContext, useState, useCallback, useEffect, useRef, lazy, Suspense } from "react";
+import React, { useContext, useState, useCallback, useEffect, useRef, lazy, Suspense } from "react";
import {
Box,
IconButton,
@@ -84,9 +84,6 @@ const ToolbarButtons: React.FC = ({
currentBranch,
defaultBranch,
currentPath,
- branches: _branches,
- branchLoading: _branchLoading,
- branchError: _branchError,
setCurrentBranch,
refreshBranches,
setCurrentPath,
diff --git a/src/components/layout/hooks/useBreadcrumbLayout.ts b/src/components/layout/hooks/useBreadcrumbLayout.ts
index 97db2d9..60d361d 100644
--- a/src/components/layout/hooks/useBreadcrumbLayout.ts
+++ b/src/components/layout/hooks/useBreadcrumbLayout.ts
@@ -1,4 +1,5 @@
import { useMemo, useRef } from 'react';
+import type { RefObject } from 'react';
import type { BreadcrumbSegment } from '@/types';
interface UseBreadcrumbLayoutOptions {
@@ -8,7 +9,7 @@ interface UseBreadcrumbLayoutOptions {
interface UseBreadcrumbLayoutReturn {
breadcrumbsMaxItems: number;
- breadcrumbsContainerRef: React.RefObject;
+ breadcrumbsContainerRef: RefObject;
}
/**
diff --git a/src/components/preview/image/ImagePreviewContent.tsx b/src/components/preview/image/ImagePreviewContent.tsx
index 81b8223..8f53a2c 100644
--- a/src/components/preview/image/ImagePreviewContent.tsx
+++ b/src/components/preview/image/ImagePreviewContent.tsx
@@ -270,7 +270,7 @@ const ImagePreviewContent: React.FC = ({
>
{({ zoomIn, zoomOut, resetTransform }) => (
<>
- {/* 图片内容 */}
+ {/* 图片展示 */}
;
@@ -12,7 +12,7 @@ interface UseDesktopNavigationOptions {
interface UseDesktopNavigationReturn {
activeNavSide: 'left' | 'right' | null;
- handleContainerMouseMove: (e: React.MouseEvent) => void;
+ handleContainerMouseMove: (e: MouseEvent) => void;
handleContainerMouseLeave: () => void;
}
@@ -31,7 +31,7 @@ export function useDesktopNavigation({
}: UseDesktopNavigationOptions): UseDesktopNavigationReturn {
const [activeNavSide, setActiveNavSide] = useState<'left' | 'right' | null>(null);
- const handleContainerMouseMove = useCallback((e: React.MouseEvent): void => {
+ const handleContainerMouseMove = useCallback((e: MouseEvent): void => {
if (isSmallScreen || hasError || loading) {
setActiveNavSide(null);
return;
diff --git a/src/components/preview/image/hooks/useKeyboardNavigation.ts b/src/components/preview/image/hooks/useKeyboardNavigation.ts
index ae17f1d..def1ff8 100644
--- a/src/components/preview/image/hooks/useKeyboardNavigation.ts
+++ b/src/components/preview/image/hooks/useKeyboardNavigation.ts
@@ -12,7 +12,7 @@ interface UseKeyboardNavigationOptions {
/**
* 键盘导航 Hook
*
- * 处理键盘左右箭头切换图片
+ * 处理键盘左右方向键切换图片
*/
export function useKeyboardNavigation({
loading,
diff --git a/src/components/preview/image/hooks/useTouchNavigation.ts b/src/components/preview/image/hooks/useTouchNavigation.ts
index 569ec8e..f2d0691 100644
--- a/src/components/preview/image/hooks/useTouchNavigation.ts
+++ b/src/components/preview/image/hooks/useTouchNavigation.ts
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
+import type { TouchEvent } from 'react';
interface TouchStart {
x: number;
@@ -21,8 +22,8 @@ interface UseTouchNavigationOptions {
interface UseTouchNavigationReturn {
dragOffset: number;
isDragging: boolean;
- handleTouchStart: (e: React.TouchEvent) => void;
- handleTouchMove: (e: React.TouchEvent) => void;
+ handleTouchStart: (e: TouchEvent) => void;
+ handleTouchMove: (e: TouchEvent) => void;
handleTouchEnd: () => void;
}
@@ -59,7 +60,7 @@ export function useTouchNavigation({
};
}, [imageUrl]);
- const handleTouchStart = (e: React.TouchEvent): void => {
+ const handleTouchStart = (e: TouchEvent): void => {
// 只在移动端、未放大、且未加载错误时启用
if (!isSmallScreen || currentScale !== 1 || hasError || loading) {
return;
@@ -75,7 +76,7 @@ export function useTouchNavigation({
}
};
- const handleTouchMove = (e: React.TouchEvent): void => {
+ const handleTouchMove = (e: TouchEvent): void => {
if (touchStart === null || !isSmallScreen || currentScale !== 1 || hasError || loading) {
return;
}
diff --git a/src/components/preview/image/types.ts b/src/components/preview/image/types.ts
index 25dbd43..c9824b2 100644
--- a/src/components/preview/image/types.ts
+++ b/src/components/preview/image/types.ts
@@ -174,7 +174,7 @@ export interface ImagePreviewContentProps {
onPrevious?: (() => void) | undefined;
/** 切换到下一张图片的回调 */
onNext?: (() => void) | undefined;
- /** 初始宽高比(用于占位与渐进过渡) */
+ /** 初始宽高比(用于占位和渐进式过渡) */
initialAspectRatio?: number | null;
/** 宽高比变更时回调 */
onAspectRatioChange?: (aspectRatio: number) => void;
diff --git a/src/components/preview/markdown/MarkdownPreview.tsx b/src/components/preview/markdown/MarkdownPreview.tsx
index 4dab6eb..743a56c 100644
--- a/src/components/preview/markdown/MarkdownPreview.tsx
+++ b/src/components/preview/markdown/MarkdownPreview.tsx
@@ -93,7 +93,7 @@ const MarkdownPreview = memo(
if (!shouldRender || !hasReadmeContent || isThemeChanging) {
return;
}
- const renderKey = `${previewPath}:${readmeContent ?? ""}`;
+ const renderKey = `${previewPath}:${readmeContent}`;
if (renderCompleteRef.current === renderKey) {
return;
}
diff --git a/src/components/preview/markdown/components/MarkdownCodeBlock.tsx b/src/components/preview/markdown/components/MarkdownCodeBlock.tsx
index 5804156..f0c592a 100644
--- a/src/components/preview/markdown/components/MarkdownCodeBlock.tsx
+++ b/src/components/preview/markdown/components/MarkdownCodeBlock.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from "react";
+import React, { useMemo, useState } from "react";
import type { ClassAttributes, HTMLAttributes } from "react";
import { Box, IconButton, Tooltip, useTheme, useMediaQuery } from "@mui/material";
import { alpha } from "@mui/material/styles";
diff --git a/src/components/preview/markdown/styles/markdownStyles.ts b/src/components/preview/markdown/styles/markdownStyles.ts
index 6c6b88b..9813e2d 100644
--- a/src/components/preview/markdown/styles/markdownStyles.ts
+++ b/src/components/preview/markdown/styles/markdownStyles.ts
@@ -232,7 +232,7 @@ export const createMarkdownStyles = (theme: Theme, latexCount: number, isSmallSc
fontFamily: MONO_FONT,
fontSize: { xs: "0.8125rem", sm: "0.875rem" },
lineHeight: 1.55,
- backgroundColor: codeSurfaceColor, // 行间代码块背景色
+ backgroundColor: codeSurfaceColor, // 代码块背景色
borderRadius: "inherit",
border: `1px solid ${codeBorderColor}`,
padding: theme.spacing(1.5, 1.75),
diff --git a/src/components/preview/markdown/utils/imageUtils.ts b/src/components/preview/markdown/utils/imageUtils.ts
index b1fcd4a..f353472 100644
--- a/src/components/preview/markdown/utils/imageUtils.ts
+++ b/src/components/preview/markdown/utils/imageUtils.ts
@@ -200,7 +200,7 @@ export const handleImageError = (
const directSrc = tryDirectImageLoad(imgSrc);
if (typeof directSrc === "string" && directSrc.length > 0) {
logger.info("尝试使用直接URL加载:", directSrc);
- // 设置新的超时计时器
+ // 设置新的超时定时器
const newTimerId = window.setTimeout(() => {
if (!imageState.loadedImages.has(directSrc)) {
imageState.failedImages.add(imgSrc);
diff --git a/src/components/preview/text/TextPreview.tsx b/src/components/preview/text/TextPreview.tsx
index 6fff1da..bb9de6d 100644
--- a/src/components/preview/text/TextPreview.tsx
+++ b/src/components/preview/text/TextPreview.tsx
@@ -1,4 +1,4 @@
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
import {
Box,
CircularProgress,
@@ -9,6 +9,7 @@ import {
Typography,
useTheme,
} from "@mui/material";
+import { List as VirtualList, type RowComponentProps, useDynamicRowHeight } from "react-window";
import { alpha } from "@mui/material/styles";
import CloseIcon from "@mui/icons-material/Close";
import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded";
@@ -18,26 +19,29 @@ import TextRotationNoneIcon from "@mui/icons-material/TextRotationNone";
import type { TextPreviewProps } from "./types";
import { formatFileSize } from "@/utils/format/formatters";
import { useI18n } from "@/contexts/I18nContext";
-import { highlightCodeByFilename } from "@/utils/content/prismHighlighter";
+import { highlightLines } from "@/utils/content/prismHighlighter";
+import { detectLanguage } from "@/utils/content/languageDetector";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
+import { useContainerSize } from "@/components/preview/image/hooks";
const MONO_FONT_STACK =
"'JetBrains Mono', 'Fira Code', 'SFMono-Regular', ui-monospace, 'Source Code Pro', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
-const TextPreview: React.FC = memo(
- ({ content, loading, isSmallScreen, previewingItem, onClose }) => {
+interface TextPreviewContentProps extends Omit {
+ content: string;
+}
+
+const TextPreviewContent: React.FC = memo(
+ ({ content, isSmallScreen, previewingItem, onClose }) => {
const theme = useTheme();
const { t } = useI18n();
const [wrapText, setWrapText] = useState(false);
- const { copied, copy, reset } = useCopyToClipboard();
- const [prevContent, setPrevContent] = useState(content);
-
- // 当 content 变化时,重置 UI 状态
- if (content !== prevContent) {
- setPrevContent(content);
- setWrapText(false);
- reset();
- }
+ const { copied, copy } = useCopyToClipboard();
+ // 小屏/桌面字号与控件尺寸统一管理,避免分散调整
+ const contentFontSize = isSmallScreen ? "0.78rem" : "0.9rem";
+ const lineNumberFontSize = isSmallScreen ? "0.7rem" : "0.9rem";
+ const controlButtonSize = isSmallScreen ? 26 : 32;
+ const controlIconSize = isSmallScreen ? 14 : 18;
const normalizedLines = useMemo(() => {
if (typeof content !== "string") {
@@ -47,27 +51,76 @@ const TextPreview: React.FC = memo(
}, [content]);
const lineCount = normalizedLines.length === 0 ? 1 : normalizedLines.length;
+ const previewingName = previewingItem?.name ?? null;
+ const language = useMemo(() => {
+ if (previewingName === null || previewingName.length === 0) {
+ return null;
+ }
+ return detectLanguage(previewingName);
+ }, [previewingName]);
- // 大于500行的文件禁用代码高亮
- const MAX_LINES_FOR_HIGHLIGHT = 500;
- const shouldHighlight = lineCount <= MAX_LINES_FOR_HIGHLIGHT;
+ const [highlightedLines, setHighlightedLines] = useState([]);
- // 计算高亮后的代码行
- const highlightedLines = useMemo(() => {
- if (typeof content !== "string" || content.length === 0) {
- return [];
- }
- // 如果行数超过限制,不进行高亮
- if (!shouldHighlight) {
- return [];
+ useEffect(() => {
+ let cancelled = false;
+
+ // 语法高亮计算开销较大,尽量在空闲时间执行以保证首屏响应
+ const runHighlight = (): void => {
+ if (normalizedLines.length === 0) {
+ if (!cancelled) {
+ setHighlightedLines([]);
+ }
+ return;
+ }
+ const result = highlightLines(normalizedLines, language);
+ if (!cancelled) {
+ setHighlightedLines(result);
+ }
+ };
+
+ if (typeof window !== "undefined") {
+ const idleCallback = window as Window & {
+ requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;
+ cancelIdleCallback?: (handle: number) => void;
+ };
+
+ if (typeof idleCallback.requestIdleCallback === "function") {
+ const handle = idleCallback.requestIdleCallback(() => {
+ runHighlight();
+ }, { timeout: 700 });
+
+ return () => {
+ cancelled = true;
+ if (typeof idleCallback.cancelIdleCallback === "function") {
+ idleCallback.cancelIdleCallback(handle);
+ }
+ };
+ }
}
- const filename = previewingItem?.name ?? undefined;
- return highlightCodeByFilename(content, filename);
- }, [content, previewingItem?.name, shouldHighlight]);
+
+ const timer = window.setTimeout(() => {
+ runHighlight();
+ }, 0);
+
+ return () => {
+ cancelled = true;
+ window.clearTimeout(timer);
+ };
+ }, [normalizedLines, language]);
+
+ // 预先转义文本,避免滚动过程中反复计算
+ const escapedLines = useMemo(() => {
+ return normalizedLines.map((line) => {
+ if (line.length === 0) {
+ return "\u00A0";
+ }
+ return line.replace(/&/g, "&").replace(//g, ">");
+ });
+ }, [normalizedLines]);
const charCount = useMemo(() => (typeof content === "string" ? content.length : 0), [content]);
- // 计算实际字节大小(UTF-8编码)
+ // 计算实际字节大小(UTF-8 编码)
const byteSize = useMemo(() => {
if (typeof content !== "string") {
return 0;
@@ -88,6 +141,144 @@ const TextPreview: React.FC = memo(
return digitCount.toString() + "ch";
}, [lineCount]);
+ const { containerRef, containerSize } = useContainerSize();
+
+ const baseRowHeight = useMemo(() => {
+ const fontSizePx = parseFloat(contentFontSize) * theme.typography.fontSize;
+ const verticalPaddingPx = parseFloat(theme.spacing(0.5));
+ return fontSizePx * 1.6 + verticalPaddingPx;
+ }, [contentFontSize, theme]);
+
+ const [viewportHeight, setViewportHeight] = useState(() => {
+ if (typeof window === "undefined") {
+ return null;
+ }
+ return window.innerHeight;
+ });
+
+ useEffect(() => {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ const handleResize = (): void => {
+ setViewportHeight(window.innerHeight);
+ };
+
+ window.addEventListener("resize", handleResize);
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ };
+ }, []);
+
+ const maxContainerHeight = useMemo(() => {
+ const fallbackHeight = 640;
+ const windowHeight = viewportHeight ?? fallbackHeight;
+ const reservedSpace = isSmallScreen ? 220 : 280;
+ return Math.max(220, windowHeight - reservedSpace);
+ }, [isSmallScreen, viewportHeight]);
+
+ const listHeight = useMemo(() => {
+ const estimated = lineCount * baseRowHeight;
+ return Math.min(maxContainerHeight, estimated);
+ }, [baseRowHeight, lineCount, maxContainerHeight]);
+
+ const listWidth = useMemo(() => {
+ return containerSize.width > 0 ? containerSize.width : 1;
+ }, [containerSize.width]);
+
+ const rowHeightCacheKey = useMemo(() => {
+ return `${String(wrapText)}-${String(containerSize.width)}-${contentFontSize}-${lineNumberColumnWidth}-${String(normalizedLines.length)}`;
+ }, [wrapText, containerSize.width, contentFontSize, lineNumberColumnWidth, normalizedLines.length]);
+
+ // 自动换行时使用动态行高缓存,避免重新计算整表
+ const dynamicRowHeight = useDynamicRowHeight({
+ defaultRowHeight: baseRowHeight,
+ key: rowHeightCacheKey,
+ });
+ const rowHeight = wrapText ? dynamicRowHeight : baseRowHeight;
+ const emptyRowProps = useMemo(() => ({}), []);
+
+ const Row = ({ index, style, ariaAttributes }: RowComponentProps): React.ReactElement => {
+ const lineHtml =
+ index < highlightedLines.length
+ ? (highlightedLines[index] ?? "\u00A0")
+ : (escapedLines[index] ?? "\u00A0");
+
+ return (
+
+
+ {index + 1}
+
+
+
+
+
+ );
+ };
+
const handleCopy = (): void => {
if (typeof content === "string") {
void copy(content);
@@ -132,8 +323,6 @@ const TextPreview: React.FC = memo(
};
}, [theme.palette.mode, theme.palette.background.paper]);
- const containerRef = useRef(null);
-
const handleCloseOptimized = useCallback(() => {
if (containerRef.current !== null) {
const container = containerRef.current;
@@ -150,7 +339,7 @@ const TextPreview: React.FC = memo(
}
}, 0);
});
- }, [onClose]);
+ }, [containerRef, onClose]);
useEffect(() => {
const container = containerRef.current;
@@ -173,29 +362,7 @@ const TextPreview: React.FC = memo(
// 更新文本颜色变量
container.style.setProperty('--text-primary', theme.palette.text.primary);
}
- }, [prismTheme, theme.palette.text.primary]);
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (typeof content !== "string") {
- return null;
- }
+ }, [containerRef, prismTheme, theme.palette.text.primary]);
return (
@@ -203,7 +370,7 @@ const TextPreview: React.FC = memo(
styles={{
".text-preview__code-table": {
fontFamily: MONO_FONT_STACK,
- fontSize: isSmallScreen ? "0.85rem" : "0.9rem",
+ fontSize: contentFontSize,
lineHeight: 1.6,
color: "var(--text-primary)",
},
@@ -352,10 +519,11 @@ const TextPreview: React.FC = memo(
= memo(
}}
data-oid="text-preview-header"
>
-
+
= memo(
-
+
= memo(
data-oid="text-preview-wrap"
>
{wrapText ? (
-
+
) : (
-
+
)}
@@ -426,8 +610,8 @@ const TextPreview: React.FC = memo(
size="small"
onClick={handleCopy}
sx={{
- width: { xs: 28, sm: 32 },
- height: { xs: 28, sm: 32 },
+ width: controlButtonSize,
+ height: controlButtonSize,
borderRadius: 2,
border: `1px solid ${alpha(theme.palette.divider, 0.7)}`,
color: copied ? theme.palette.success.main : theme.palette.text.secondary,
@@ -443,7 +627,11 @@ const TextPreview: React.FC = memo(
}}
data-oid="text-preview-copy"
>
- {copied ? : }
+ {copied ? (
+
+ ) : (
+
+ )}
@@ -454,100 +642,28 @@ const TextPreview: React.FC = memo(
className="text-preview__code-container"
sx={{
width: "100%",
- maxHeight: isSmallScreen ? "calc(100vh - 220px)" : "calc(100vh - 280px)",
- overflow: "auto",
+ height: listHeight,
+ maxHeight: maxContainerHeight,
+ overflow: "hidden",
backgroundColor: prismTheme.background,
}}
data-oid="text-preview-content"
>
-
-
-
- {normalizedLines.map((line, index) => (
-
- |
- {index + 1}
- |
-
- 0
- ? line.replace(/&/g, "&").replace(//g, ">")
- : "\u00A0",
- }}
- />
- |
-
- ))}
-
-
-
+ />
@@ -555,6 +671,50 @@ const TextPreview: React.FC = memo(
},
);
+TextPreviewContent.displayName = "TextPreviewContent";
+
+const TextPreview: React.FC = memo(
+ ({ content, loading, isSmallScreen, previewingItem, onClose }) => {
+ const contentKey = useMemo(() => {
+ const safeContent = typeof content === "string" ? content : "";
+ const pathKey = previewingItem?.path ?? "";
+ return `${pathKey}::${safeContent}`;
+ }, [content, previewingItem?.path]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (typeof content !== "string") {
+ return null;
+ }
+
+ return (
+
+ );
+ },
+);
+
TextPreview.displayName = "TextPreview";
export default TextPreview;
diff --git a/src/components/seo/DynamicSEO.tsx b/src/components/seo/DynamicSEO.tsx
index 60385ce..9a9cc9a 100644
--- a/src/components/seo/DynamicSEO.tsx
+++ b/src/components/seo/DynamicSEO.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import React, { useEffect } from "react";
import { useSEO } from "@/contexts/SEOContext/useSEO";
import SEO from "./SEO";
diff --git a/src/components/ui/ErrorBoundary.tsx b/src/components/ui/ErrorBoundary.tsx
index 89d0d90..00dd63a 100644
--- a/src/components/ui/ErrorBoundary.tsx
+++ b/src/components/ui/ErrorBoundary.tsx
@@ -149,11 +149,7 @@ class ErrorBoundary extends React.Component {
- this.resetTimeoutId = window.setTimeout(() => {
- this.resetError();
- }, delay);
- };
+
toggleDetails = (): void => {
this.setState(prevState => ({
@@ -423,4 +419,4 @@ export const FeatureErrorBoundary: React.FC<{
);
-export default ErrorBoundary;
+
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
index 7b2f003..e0dee39 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -1,8 +1,2 @@
export { default as BranchSwitcher } from './BranchSwitcher';
-export { default as CustomSnackbar } from './CustomSnackbar';
-export { default as DynamicIcon } from './DynamicIcon';
-export { default as EmptyState } from './EmptyState';
-export { default as ErrorDisplay } from './ErrorDisplay';
-export { default as FaviconManager } from './DynamicIcon';
-export { default as LoadingSpinner } from './LoadingSpinner';
export * from './skeletons';
diff --git a/src/config/core/ConfigLoader.ts b/src/config/core/ConfigLoader.ts
index 694fea1..e938d92 100644
--- a/src/config/core/ConfigLoader.ts
+++ b/src/config/core/ConfigLoader.ts
@@ -162,18 +162,6 @@ export class ConfigLoader {
return result;
}
- /**
- * 获取字符串类型的环境变量
- */
- public getEnvString(env: EnvSource, key: string): string | undefined {
- const value = env[key];
- if (typeof value !== 'string') {
- return undefined;
- }
- const trimmed = value.trim();
- return trimmed.length > 0 ? trimmed : undefined;
- }
-
/**
* 获取布尔标志
*/
diff --git a/src/config/core/ConfigManager.ts b/src/config/core/ConfigManager.ts
index 8923d5f..a1ebfe2 100644
--- a/src/config/core/ConfigManager.ts
+++ b/src/config/core/ConfigManager.ts
@@ -42,26 +42,6 @@ export class ConfigManager {
return this.instance;
}
- /**
- * 重置单例实例(用于测试)
- *
- * 在测试环境中重置单例实例,确保每个测试的独立性。
- *
- * @warning 仅在测试环境中使用,生产环境不应调用此方法
- */
- static resetInstance(): void {
- ConfigManager.instance = null;
- }
-
- /**
- * 检查是否已创建实例
- *
- * @returns 如果实例已创建返回 true
- */
- static hasInstance(): boolean {
- return ConfigManager.instance !== null;
- }
-
/**
* 获取配置
*
@@ -146,41 +126,6 @@ export class ConfigManager {
}
}
- /**
- * 禁用配置热更新
- *
- * 移除配置热更新监听器。
- *
- * @returns void
- */
- disableHotReload(): void {
- if (!this.hotReloadEnabled || this.hotReloadHandler === null) {
- return;
- }
-
- if (typeof window !== 'undefined') {
- window.removeEventListener('config:reload', this.hotReloadHandler);
- }
-
- this.hotReloadHandler = null;
- this.hotReloadEnabled = false;
-
- const config = this.getConfig();
- if (config.developer.mode || config.developer.consoleLogging) {
- // eslint-disable-next-line no-console
- console.log('[ConfigManager] 配置热更新已禁用');
- }
- }
-
- /**
- * 检查是否启用了热更新
- *
- * @returns 如果热更新已启用返回 true
- */
- isHotReloadEnabled(): boolean {
- return this.hotReloadEnabled;
- }
-
// 通知配置变更
private notifyConfigChange(newConfig: Config, oldConfig: Config): void {
this.listeners.forEach(listener => {
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 398578f..41d581f 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -2,16 +2,3 @@ import { getSiteConfig } from '../config';
/** 网站标题 */
export const SITE_TITLE = getSiteConfig().title;
-
-/** API常量配置 */
-export const API_CONSTANTS = {
- GITHUB_API_URL: 'https://api.github.com',
- DEFAULT_PER_PAGE: 100
-};
-
-/** 本地存储键名 */
-export const STORAGE_KEYS = {
- COLOR_MODE: 'colorMode',
- RECENT_REPOS: 'recentRepos',
- ACCESS_TOKEN: 'accessToken'
-};
diff --git a/src/contexts/ColorModeProvider.tsx b/src/contexts/ColorModeProvider.tsx
deleted file mode 100644
index a209892..0000000
--- a/src/contexts/ColorModeProvider.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, { useMemo, useState, type ReactNode } from "react";
-import { type PaletteMode, useMediaQuery } from "@mui/material";
-import { ColorModeContext } from "./colorModeContext";
-
-/**
- * 颜色模式提供者组件属性接口
- */
-interface ColorModeProviderProps {
- children: ReactNode;
-}
-
-/**
- * 颜色模式提供者组件
- *
- * 提供明暗主题切换功能和自动模式管理。
- */
-export const ColorModeProvider: React.FC = ({
- children,
-}) => {
- const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
- const [mode, setMode] = useState(() => {
- const savedMode = localStorage.getItem("colorMode");
- if (savedMode === "light" || savedMode === "dark") {
- return savedMode;
- }
- return prefersDarkMode ? "dark" : "light";
- });
- const [isAutoMode, setIsAutoMode] = useState(false);
-
- const colorMode = useMemo(
- () => ({
- toggleColorMode: () => {
- setMode((prevMode) => {
- const newMode = prevMode === "light" ? "dark" : "light";
- localStorage.setItem("colorMode", newMode);
- return newMode;
- });
- },
- toggleAutoMode: () => {
- setIsAutoMode((prev) => !prev);
- },
- mode,
- isAutoMode,
- }),
- [mode, isAutoMode],
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/src/contexts/SEOContext/index.tsx b/src/contexts/SEOContext/index.tsx
index 0a06217..3a2290f 100644
--- a/src/contexts/SEOContext/index.tsx
+++ b/src/contexts/SEOContext/index.tsx
@@ -1,2 +1 @@
-export { MetadataProvider as SEOProvider } from "@/contexts/MetadataContext";
export { MetadataProvider as default } from "@/contexts/MetadataContext";
diff --git a/src/hooks/github/useProgressiveLoading.ts b/src/hooks/github/useProgressiveLoading.ts
index b5df3ec..cb0ff5c 100644
--- a/src/hooks/github/useProgressiveLoading.ts
+++ b/src/hooks/github/useProgressiveLoading.ts
@@ -1,119 +1 @@
-import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
-
-interface UseProgressiveLoadingOptions {
- /** 异步加载函数 */
- loadFn: () => Promise;
- /** 触发器(依赖项),变化时重新加载 */
- trigger?: unknown;
- /** 首次加载占位延迟(ms),默认 500 */
- initialPlaceholderDelay?: number;
- /** 二次加载占位延迟(ms),默认 300 */
- subsequentPlaceholderDelay?: number;
- /** 是否启用渐进式加载 */
- enabled?: boolean;
-}
-
-interface UseProgressiveLoadingReturn {
- /** 加载状态 */
- loading: boolean;
- /** 错误信息 */
- error: Error | null;
- /** 加载的数据 */
- data: T | null;
- /** 是否显示占位符 */
- showPlaceholder: boolean;
- /** 重新加载 */
- reload: () => void;
-}
-
-/**
- * 渐进式加载 Hook
- *
- * - 首次加载:500ms 后才显示占位组件
- * - 二次加载:300ms 后显示占位组件
- * - 快速响应时不显示加载状态,减少闪烁
- */
-export function useProgressiveLoading({
- loadFn,
- trigger,
- initialPlaceholderDelay = 500,
- subsequentPlaceholderDelay = 300,
- enabled = true,
-}: UseProgressiveLoadingOptions): UseProgressiveLoadingReturn {
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [data, setData] = useState(null);
- const [showPlaceholder, setShowPlaceholder] = useState(false);
-
- const hasLoadedOnceRef = useRef(false);
- const placeholderTimerRef = useRef(null);
- const isMountedRef = useRef(true);
-
- const clearPlaceholderTimer = useCallback(() => {
- if (placeholderTimerRef.current !== null) {
- window.clearTimeout(placeholderTimerRef.current);
- placeholderTimerRef.current = null;
- }
- }, []);
-
- const load = useCallback(async () => {
- if (!enabled) {
- return;
- }
-
- setLoading(true);
- setError(null);
- clearPlaceholderTimer();
-
- // 根据是否首次加载决定延迟时间
- const delay = hasLoadedOnceRef.current ? subsequentPlaceholderDelay : initialPlaceholderDelay;
-
- // 延迟显示占位符
- placeholderTimerRef.current = window.setTimeout(() => {
- if (isMountedRef.current) {
- setShowPlaceholder(true);
- }
- }, delay);
-
- try {
- const result = await loadFn();
-
- if (isMountedRef.current) {
- setData(result);
- setError(null);
- hasLoadedOnceRef.current = true;
- }
- } catch (err) {
- if (isMountedRef.current) {
- setError(err instanceof Error ? err : new Error(String(err)));
- }
- } finally {
- if (isMountedRef.current) {
- setLoading(false);
- clearPlaceholderTimer();
- setShowPlaceholder(false);
- }
- }
- }, [enabled, loadFn, initialPlaceholderDelay, subsequentPlaceholderDelay, clearPlaceholderTimer]);
-
- useEffect(() => {
- isMountedRef.current = true;
- void load();
-
- return () => {
- isMountedRef.current = false;
- clearPlaceholderTimer();
- };
- }, [trigger, load, clearPlaceholderTimer]);
-
- return useMemo(() => ({
- loading,
- error,
- data,
- showPlaceholder,
- reload: (): void => {
- void load();
- },
- }), [loading, error, data, showPlaceholder, load]);
-}
-
+export {};
diff --git a/src/hooks/github/useReadmeContent.ts b/src/hooks/github/useReadmeContent.ts
index 7c5aaff..5a53a27 100644
--- a/src/hooks/github/useReadmeContent.ts
+++ b/src/hooks/github/useReadmeContent.ts
@@ -8,9 +8,9 @@ import type { ReadmeContentState } from './types';
/**
* README 内容管理 Hook
- *
+ *
* 管理 README 文件的加载和状态
- *
+ *
* @param contents - 当前目录的内容列表
* @param currentPath - 当前路径
* @param currentBranch - 当前分支
@@ -81,7 +81,7 @@ export function useReadmeContent(contents: GitHubContent[], currentPath: string,
.split('/')
.map(segment => encodeURIComponent(segment))
.join('/');
- const cacheTag = typeof readmeItem.sha === 'string' && readmeItem.sha.length > 0
+ const cacheTag = readmeItem.sha.length > 0
? readmeItem.sha
: requestKey;
const baseUrl = `https://raw.githubusercontent.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/${encodeURIComponent(currentBranchRef.current)}/${encodedPath}`;
diff --git a/src/hooks/github/useRepoSearch/types.ts b/src/hooks/github/useRepoSearch/types.ts
index c47b4c7..76999d3 100644
--- a/src/hooks/github/useRepoSearch/types.ts
+++ b/src/hooks/github/useRepoSearch/types.ts
@@ -1,5 +1,5 @@
import type { GitHubContent } from '@/types';
-import type { SearchIndexResultItem } from '@/services/github/core/searchIndex';
+import type { SearchIndexErrorCode, SearchIndexResultItem } from '@/services/github/core/searchIndex';
export type RepoSearchMode = 'search-index' | 'github-api';
@@ -37,7 +37,7 @@ export interface RepoSearchExecutionResult {
export interface RepoSearchError {
source: 'index' | 'search';
- code?: string;
+ code?: SearchIndexErrorCode;
message: string;
details?: unknown;
raw?: unknown;
@@ -88,4 +88,3 @@ export interface UseRepoSearchOptions {
defaultBranch: string;
branches: string[];
}
-
diff --git a/src/hooks/github/useRepoSearch/utils.ts b/src/hooks/github/useRepoSearch/utils.ts
index 64492bc..c3658bc 100644
--- a/src/hooks/github/useRepoSearch/utils.ts
+++ b/src/hooks/github/useRepoSearch/utils.ts
@@ -78,7 +78,6 @@ export function normalizeSearchIndexError(error: unknown): RepoSearchError {
const message = error instanceof Error ? error.message : 'Unknown search index error';
return {
source: 'index',
- code: 'UNKNOWN',
message,
raw: error
} satisfies RepoSearchError;
@@ -96,11 +95,18 @@ export function normalizeSearchError(error: unknown, mode: RepoSearchMode): Repo
}
const message = error instanceof Error ? error.message : 'Unknown search error';
- return {
+ const base: RepoSearchError = {
source: 'search',
- code: mode === 'search-index' ? SearchIndexErrorCode.INDEX_FILE_NOT_FOUND : 'UNKNOWN',
message,
raw: error
- } satisfies RepoSearchError;
-}
+ };
+
+ if (mode === 'search-index') {
+ return {
+ ...base,
+ code: SearchIndexErrorCode.INDEX_FILE_NOT_FOUND
+ } satisfies RepoSearchError;
+ }
+ return base;
+}
diff --git a/src/hooks/useCopyToClipboard.ts b/src/hooks/useCopyToClipboard.ts
index c0560fa..78a54fe 100644
--- a/src/hooks/useCopyToClipboard.ts
+++ b/src/hooks/useCopyToClipboard.ts
@@ -43,7 +43,10 @@ export function useCopyToClipboard(
async (text: string): Promise => {
try {
if (typeof navigator === "undefined") {
- throw new Error("navigator 未定义");
+ const error = new Error("navigator 未定义");
+ logger.error("复制失败:", error);
+ onError?.(error);
+ return false;
}
const clipboard = navigator.clipboard as Clipboard | undefined;
diff --git a/src/hooks/useDownload.ts b/src/hooks/useDownload.ts
index 48ce2c9..b7a85a2 100644
--- a/src/hooks/useDownload.ts
+++ b/src/hooks/useDownload.ts
@@ -146,18 +146,23 @@ export const useDownload = (onError: (message: string) => void): {
// 检查是否已取消 (ref可在异步期间被cancelDownload修改)
if (hasBeenCancelled()) {
- throw new Error('下载已取消');
+ logger.info('文件下载已取消');
+ return;
}
if (!response.ok) {
- throw new Error(`下载失败: ${String(response.status)} ${response.statusText}`);
+ const error = new Error(`下载失败: ${String(response.status)} ${response.statusText}`);
+ logger.error('下载文件失败:', error);
+ onError(t('error.file.downloadFailed', { message: error.message }));
+ return;
}
const blob = await response.blob();
// 再次检查是否已取消 (ref可在异步期间被cancelDownload修改)
if (hasBeenCancelled()) {
- throw new Error('下载已取消');
+ logger.info('文件下载已取消');
+ return;
}
saveAs(blob, item.name);
@@ -194,7 +199,7 @@ export const useDownload = (onError: (message: string) => void): {
// 检查是否已取消 (ref可在异步期间被cancelDownload修改)
if (hasBeenCancelled()) {
- throw new Error('Download cancelled');
+ return;
}
// 处理每个文件/文件夹
@@ -221,12 +226,15 @@ export const useDownload = (onError: (message: string) => void): {
} else {
// 递归处理子文件夹 (type === 'dir')
await collectFilesInner(item.path, fileList, basePath, signal);
+ if (hasBeenCancelled()) {
+ return;
+ }
}
}
} catch (e) {
// 如果是取消导致的错误,抛出
if (e instanceof Error && (e.name === 'AbortError' || hasBeenCancelled())) {
- throw e;
+ return;
}
// 其他错误记录但继续处理
logger.error(`获取文件夹内容失败: ${folderPath}`, e);
@@ -260,7 +268,8 @@ export const useDownload = (onError: (message: string) => void): {
// 检查是否已取消 (ref可在异步期间被cancelDownload修改)
if (hasBeenCancelled()) {
- throw new Error('下载已取消');
+ logger.info('文件夹下载已取消');
+ return;
}
dispatch({ type: 'SET_TOTAL_FILES', count: allFiles.length });
@@ -272,13 +281,15 @@ export const useDownload = (onError: (message: string) => void): {
try {
// 检查是否已取消 (ref可在异步期间被cancelDownload修改)
if (hasBeenCancelled()) {
- throw new Error('下载已取消');
+ logger.info('文件夹下载已取消');
+ return;
}
const response = await fetch(file.url, { signal });
if (!response.ok) {
- throw new Error(`下载失败: ${String(response.status)}`);
+ logger.error(`文件 ${file.path} 下载失败:`, new Error(`下载失败: ${String(response.status)}`));
+ continue;
}
const blob = await response.blob();
@@ -293,14 +304,16 @@ export const useDownload = (onError: (message: string) => void): {
} catch (e) {
// 检查是否是取消导致的错误
if (e instanceof Error && (e.name === 'AbortError' || hasBeenCancelled())) {
- throw e; // 重新抛出以中断循环
+ logger.info('文件夹下载已取消');
+ return;
}
logger.error(`文件 ${file.path} 下载失败:`, e);
}
// 检查是否已取消 (ref可在异步期间被cancelDownload修改)
if (hasBeenCancelled()) {
- throw new Error('下载已取消');
+ logger.info('文件夹下载已取消');
+ return;
}
}
@@ -317,7 +330,8 @@ export const useDownload = (onError: (message: string) => void): {
// 最后一次检查是否已取消 (ref可在异步期间被cancelDownload修改)
if (hasBeenCancelled()) {
- throw new Error('下载已取消');
+ logger.info('文件夹下载已取消');
+ return;
}
saveAs(zipBlob, `${folderName}.zip`);
diff --git a/src/hooks/useErrorHandler.ts b/src/hooks/useErrorHandler.ts
index 85e0f63..161213d 100644
--- a/src/hooks/useErrorHandler.ts
+++ b/src/hooks/useErrorHandler.ts
@@ -230,46 +230,3 @@ export function useErrorHandler(
lastError: errors[0] ?? null
};
}
-
-/**
- * 全局错误处理Hook
- *
- * 监听全局错误事件和未处理的Promise拒绝,自动捕获并处理。
- *
- * @returns 错误处理器对象
- */
-export function useGlobalErrorHandler(): ErrorHandlerReturn {
- const globalDeveloperConfig = getDeveloperConfig();
- const errorHandler = useErrorHandler({
- showNotification: false, // 全局错误不显示通知
- logToConsole: globalDeveloperConfig.mode || globalDeveloperConfig.consoleLogging
- });
-
- useEffect(() => {
- // 监听全局错误事件
- const handleGlobalError = (event: ErrorEvent): void => {
- errorHandler.handleError(
- new Error(event.message),
- 'global_error'
- );
- };
-
- // 监听未处理的Promise拒绝
- const handleUnhandledRejection = (event: PromiseRejectionEvent): void => {
- errorHandler.handleError(
- new Error(String(event.reason)),
- 'unhandled_promise_rejection'
- );
- };
-
- window.addEventListener('error', handleGlobalError);
- window.addEventListener('unhandledrejection', handleUnhandledRejection);
-
- return () => {
- window.removeEventListener('error', handleGlobalError);
- window.removeEventListener('unhandledrejection', handleUnhandledRejection);
- };
- }, [errorHandler]);
-
- return errorHandler;
-}
diff --git a/src/hooks/useFilePreview.ts b/src/hooks/useFilePreview.ts
index f779540..cf0daf3 100644
--- a/src/hooks/useFilePreview.ts
+++ b/src/hooks/useFilePreview.ts
@@ -1,8 +1,10 @@
import { useReducer, useCallback, useRef, useState, useEffect } from 'react';
+import type { RefObject } from 'react';
import { useTheme } from '@mui/material';
import type { PreviewState, PreviewAction, GitHubContent } from '@/types';
import { GitHub } from '@/services/github';
-import { file, logger, pdf } from '@/utils';
+import { logger, pdf } from '@/utils';
+import { isImageFile, isMarkdownFile, isPdfFile, isTextFile } from '@/utils/files/fileHelpers';
import { getPreviewFromUrl, updateUrlWithHistory, hasPreviewParam } from '@/utils/routing/urlManager';
import { getForceServerProxy } from '@/services/github/config/ProxyForceManager';
import { useI18n } from '@/contexts/I18nContext';
@@ -97,7 +99,7 @@ export const useFilePreview = (
closePreview: () => void;
toggleImageFullscreen: () => void;
handleImageError: (error: string) => void;
- currentPreviewItemRef: React.RefObject;
+ currentPreviewItemRef: RefObject;
} => {
const [previewState, dispatch] = useReducer(previewReducer, initialPreviewState);
const [useTokenMode, setUseTokenMode] = useState(true);
@@ -174,7 +176,7 @@ export const useFilePreview = (
const fileNameLower = item.name.toLowerCase();
const isCurrentTarget = (): boolean => currentPreviewItemRef.current?.path === targetPath;
- if (file.isMarkdownFile(fileNameLower)) {
+ if (isMarkdownFile(fileNameLower)) {
updateUrlWithHistory(dirPath, item.path);
dispatch({ type: 'SET_PREVIEW_LOADING', loading: true });
@@ -192,7 +194,7 @@ export const useFilePreview = (
dispatch({ type: 'SET_PREVIEW_LOADING', loading: false });
}
}
- else if (file.isTextFile(item.name)) {
+ else if (isTextFile(item.name)) {
updateUrlWithHistory(dirPath, item.path);
dispatch({ type: 'SET_PREVIEW_LOADING', loading: true });
@@ -210,7 +212,7 @@ export const useFilePreview = (
dispatch({ type: 'SET_PREVIEW_LOADING', loading: false });
}
}
- else if (file.isPdfFile(fileNameLower)) {
+ else if (isPdfFile(fileNameLower)) {
// 使用新的 PDF 预览工具函数
try {
await pdf.openPDFPreview({
@@ -237,7 +239,7 @@ export const useFilePreview = (
}
return;
}
- else if (file.isImageFile(fileNameLower)) {
+ else if (isImageFile(fileNameLower)) {
// 图片预览
dispatch({ type: 'SET_IMAGE_LOADING', loading: true });
dispatch({ type: 'SET_IMAGE_ERROR', error: null });
diff --git a/src/hooks/useGitHubContentStateMachine.ts b/src/hooks/useGitHubContentStateMachine.ts
deleted file mode 100644
index 10a57ad..0000000
--- a/src/hooks/useGitHubContentStateMachine.ts
+++ /dev/null
@@ -1,403 +0,0 @@
-import { useReducer, useCallback, useEffect, useRef } from 'react';
-import type { GitHubContent } from '@/types';
-import { GitHub } from '@/services/github';
-import { logger } from '@/utils';
-import { sortContentsByPinyin } from '@/utils/sorting/contentSorting';
-import {
- getBranchFromUrl,
- getPathFromUrl,
- updateUrlWithHistory,
- updateUrlWithoutHistory
-} from '@/utils/routing/urlManager';
-import type { NavigationDirection } from '@/contexts/unified';
-
-/**
- * 内容状态类型
- */
-type ContentState =
- | { type: 'idle' }
- | { type: 'loading'; path: string; branch: string }
- | {
- type: 'loaded';
- path: string;
- branch: string;
- contents: GitHubContent[];
- readme: {
- content: string | null;
- loading: boolean;
- };
- }
- | { type: 'error'; error: string; path: string; branch: string };
-
-/**
- * 分支状态类型
- */
-type BranchState =
- | { type: 'idle'; current: string; available: string[] }
- | { type: 'loading'; current: string; available: string[] }
- | { type: 'loaded'; current: string; available: string[]; }
- | { type: 'error'; error: string; current: string; available: string[] };
-
-/**
- * 组合状态
- */
-interface State {
- content: ContentState;
- branch: BranchState;
- navigationDirection: NavigationDirection;
- requestId: number;
-}
-
-/**
- * 动作类型
- */
-type Action =
- | { type: 'START_LOADING'; path: string; branch: string; requestId: number }
- | { type: 'LOAD_SUCCESS'; contents: GitHubContent[]; requestId: number }
- | { type: 'LOAD_README'; content: string; requestId: number }
- | { type: 'README_LOADING'; loading: boolean }
- | { type: 'LOAD_ERROR'; error: string; requestId: number }
- | { type: 'SET_NAVIGATION_DIRECTION'; direction: NavigationDirection }
- | { type: 'START_BRANCH_LOADING' }
- | { type: 'BRANCH_LOAD_SUCCESS'; branches: string[] }
- | { type: 'BRANCH_LOAD_ERROR'; error: string }
- | { type: 'CHANGE_BRANCH'; branch: string };
-
-/**
- * Reducer 函数
- */
-function reducer(state: State, action: Action): State {
- switch (action.type) {
- case 'START_LOADING':
- // 只处理最新的请求
- if (action.requestId < state.requestId) {
- return state;
- }
-
- return {
- ...state,
- content: {
- type: 'loading',
- path: action.path,
- branch: action.branch
- },
- requestId: action.requestId
- };
-
- case 'LOAD_SUCCESS':
- // 只处理最新的请求
- if (action.requestId !== state.requestId) {
- return state;
- }
-
- if (state.content.type === 'loading') {
- return {
- ...state,
- content: {
- type: 'loaded',
- path: state.content.path,
- branch: state.content.branch,
- contents: action.contents,
- readme: {
- content: null,
- loading: false
- }
- }
- };
- }
- return state;
-
- case 'LOAD_README':
- if (action.requestId !== state.requestId) {
- return state;
- }
-
- if (state.content.type === 'loaded') {
- return {
- ...state,
- content: {
- ...state.content,
- readme: {
- content: action.content,
- loading: false
- }
- }
- };
- }
- return state;
-
- case 'README_LOADING':
- if (state.content.type === 'loaded') {
- return {
- ...state,
- content: {
- ...state.content,
- readme: {
- ...state.content.readme,
- loading: action.loading
- }
- }
- };
- }
- return state;
-
- case 'LOAD_ERROR':
- if (action.requestId !== state.requestId) {
- return state;
- }
-
- if (state.content.type === 'loading') {
- return {
- ...state,
- content: {
- type: 'error',
- error: action.error,
- path: state.content.path,
- branch: state.content.branch
- }
- };
- }
- return state;
-
- case 'SET_NAVIGATION_DIRECTION':
- return {
- ...state,
- navigationDirection: action.direction
- };
-
- case 'START_BRANCH_LOADING':
- if (state.branch.type === 'idle' || state.branch.type === 'loaded') {
- return {
- ...state,
- branch: {
- type: 'loading',
- current: state.branch.current,
- available: state.branch.available
- }
- };
- }
- return state;
-
- case 'BRANCH_LOAD_SUCCESS':
- if (state.branch.type === 'loading') {
- const branchSet = new Set([...state.branch.available, ...action.branches]);
- return {
- ...state,
- branch: {
- type: 'loaded',
- current: state.branch.current,
- available: Array.from(branchSet).sort()
- }
- };
- }
- return state;
-
- case 'BRANCH_LOAD_ERROR':
- if (state.branch.type === 'loading') {
- return {
- ...state,
- branch: {
- type: 'error',
- error: action.error,
- current: state.branch.current,
- available: state.branch.available
- }
- };
- }
- return state;
-
- case 'CHANGE_BRANCH':
- return {
- ...state,
- branch: {
- ...state.branch,
- current: action.branch
- },
- content: { type: 'idle' }
- };
-
- default:
- return state;
- }
-}
-
-/**
- * GitHub 内容状态机 Hook
- *
- * 使用状态机模式管理复杂的内容加载状态,简化状态同步。
- */
-export function useGitHubContentStateMachine(): {
- currentPath: string;
- currentBranch: string;
- contents: GitHubContent[];
- readmeContent: string | null;
- loading: boolean;
- loadingReadme: boolean;
- readmeLoaded: boolean;
- error: string | null;
- navigationDirection: NavigationDirection;
- branches: string[];
- branchLoading: boolean;
- branchError: string | null;
- defaultBranch: string;
- setCurrentPath: (path: string, direction?: NavigationDirection) => void;
- setCurrentBranch: (branch: string) => void;
- refreshContents: () => void;
- refreshBranches: () => Promise;
-} {
- const defaultBranch = GitHub.Branch.getDefaultBranchName();
- const initialPath = getPathFromUrl();
- const branchFromUrl = getBranchFromUrl();
- const initialBranch = branchFromUrl.length > 0 ? branchFromUrl : defaultBranch;
-
- const [state, dispatch] = useReducer(reducer, {
- content: { type: 'idle' },
- branch: {
- type: 'idle',
- current: initialBranch,
- available: [defaultBranch]
- },
- navigationDirection: 'none',
- requestId: 0
- });
-
- const requestIdCounter = useRef(0);
- const isInitialLoad = useRef(true);
-
- /**
- * 加载内容
- */
- const loadContent = useCallback(async (path: string, branch: string) => {
- const requestId = ++requestIdCounter.current;
-
- dispatch({
- type: 'START_LOADING',
- path,
- branch,
- requestId
- });
-
- try {
- const data = await GitHub.Content.getContents(path);
- const sortedData = sortContentsByPinyin(data);
-
- dispatch({
- type: 'LOAD_SUCCESS',
- contents: sortedData,
- requestId
- });
-
- // 查找 README
- const readmeItem = sortedData.find(item =>
- item.type === 'file' &&
- item.name.toLowerCase().includes('readme') &&
- item.name.toLowerCase().endsWith('.md')
- );
-
- const downloadUrl = readmeItem?.download_url;
- if (downloadUrl !== null && downloadUrl !== undefined && downloadUrl.length > 0) {
- dispatch({ type: 'README_LOADING', loading: true });
- try {
- const content = await GitHub.Content.getFileContent(downloadUrl);
- dispatch({ type: 'LOAD_README', content, requestId });
- } catch (error) {
- logger.error('加载 README 失败', error);
- }
- }
- } catch (error) {
- const message = error instanceof Error ? error.message : '未知错误';
- dispatch({ type: 'LOAD_ERROR', error: message, requestId });
- }
- }, []);
-
- /**
- * 加载分支列表
- */
- const loadBranches = useCallback(async () => {
- dispatch({ type: 'START_BRANCH_LOADING' });
-
- try {
- const branches = await GitHub.Branch.getBranches();
- dispatch({ type: 'BRANCH_LOAD_SUCCESS', branches });
- } catch (error) {
- const message = error instanceof Error ? error.message : '未知错误';
- dispatch({ type: 'BRANCH_LOAD_ERROR', error: message });
- }
- }, []);
-
- /**
- * 切换路径
- */
- const branchType = state.branch.type;
- const currentBranch = state.branch.current;
-
- const setCurrentPath = useCallback((path: string, direction: NavigationDirection = 'none') => {
- dispatch({ type: 'SET_NAVIGATION_DIRECTION', direction });
-
- if (branchType !== 'idle') {
- void loadContent(path, currentBranch);
- }
- }, [branchType, currentBranch, loadContent]);
-
- /**
- * 切换分支
- */
- const changeBranch = useCallback((branch: string) => {
- dispatch({ type: 'CHANGE_BRANCH', branch });
- GitHub.Branch.setCurrentBranch(branch);
- }, []);
-
- /**
- * 刷新内容
- */
- const refreshContents = useCallback(() => {
- if (state.content.type === 'loaded') {
- void loadContent(state.content.path, state.content.branch);
- }
- }, [state.content, loadContent]);
-
- // 初始加载
- useEffect(() => {
- if (isInitialLoad.current) {
- void loadContent(initialPath, initialBranch);
- void loadBranches();
- isInitialLoad.current = false;
- }
- }, [initialPath, initialBranch, loadContent, loadBranches]);
-
- // URL 同步
- useEffect(() => {
- if (state.content.type === 'loaded') {
- if (!isInitialLoad.current) {
- updateUrlWithHistory(state.content.path, undefined, state.content.branch);
- } else {
- updateUrlWithoutHistory(state.content.path, undefined, state.content.branch);
- }
- }
- }, [state.content]);
-
- return {
- // 状态
- currentPath: state.content.type !== 'idle' ? state.content.path : initialPath,
- currentBranch: state.branch.current,
- contents: state.content.type === 'loaded' ? state.content.contents : [],
- readmeContent: state.content.type === 'loaded' ? state.content.readme.content : null,
- loading: state.content.type === 'loading',
- loadingReadme: state.content.type === 'loaded' ? state.content.readme.loading : false,
- readmeLoaded: state.content.type === 'loaded',
- error: state.content.type === 'error' ? state.content.error : null,
- navigationDirection: state.navigationDirection,
-
- // 分支状态
- branches: state.branch.available,
- branchLoading: state.branch.type === 'loading',
- branchError: state.branch.type === 'error' ? state.branch.error : null,
- defaultBranch,
-
- // 操作
- setCurrentPath,
- setCurrentBranch: changeBranch,
- refreshContents,
- refreshBranches: loadBranches
- };
-}
diff --git a/src/hooks/useScroll.ts b/src/hooks/useScroll.ts
index 5524ec5..1fefa95 100644
--- a/src/hooks/useScroll.ts
+++ b/src/hooks/useScroll.ts
@@ -1,4 +1,4 @@
-import { useRef, useCallback, useState } from 'react';
+import { useRef, useCallback, useState, useEffect } from 'react';
/**
* 滚动数据点接口
@@ -59,6 +59,7 @@ export function useOptimizedScroll(options: ScrollSpeedOptions = {}): {
scrollEndDelay = 150,
fastScrollThreshold = 0.3
} = options;
+ const speedEpsilon = 0.001;
// 滚动状态
const [isScrolling, setIsScrolling] = useState(false);
@@ -68,11 +69,28 @@ export function useOptimizedScroll(options: ScrollSpeedOptions = {}): {
const scrollDataRef = useRef<{
positions: ScrollDataPoint[];
timer: NodeJS.Timeout | null;
+ rafId: number | null;
+ isScrolling: boolean;
+ speed: number;
}>({
positions: [],
- timer: null
+ timer: null,
+ rafId: null,
+ isScrolling: false,
+ speed: 0
});
+ const recordSample = useCallback((offset: number): void => {
+ const now = Date.now();
+ const data = scrollDataRef.current;
+
+ data.positions.push({ offset, time: now });
+
+ if (data.positions.length > maxSamples) {
+ data.positions.shift();
+ }
+ }, [maxSamples]);
+
/**
* 计算滚动速度
*
@@ -81,18 +99,9 @@ export function useOptimizedScroll(options: ScrollSpeedOptions = {}): {
* @param offset - 当前滚动偏移量
* @returns 标准化的滚动速度(像素/毫秒)
*/
- const calculateSpeed = useCallback((offset: number): number => {
- const now = Date.now();
+ const calculateSpeed = useCallback((): number => {
const data = scrollDataRef.current;
-
- // 添加新样本
- data.positions.push({ offset, time: now });
-
- // 保持固定数量的样本(FIFO)
- if (data.positions.length > maxSamples) {
- data.positions.shift();
- }
-
+
// 需要至少2个样本才能计算速度
if (data.positions.length < 2) {
return 0;
@@ -111,7 +120,7 @@ export function useOptimizedScroll(options: ScrollSpeedOptions = {}): {
// 避免除以零
return time > 0 ? distance / time : 0;
- }, [maxSamples]);
+ }, []);
/**
* 处理滚动事件
@@ -119,28 +128,42 @@ export function useOptimizedScroll(options: ScrollSpeedOptions = {}): {
* @param scrollOffset - 滚动偏移量
*/
const handleScroll = useCallback((scrollOffset: number): void => {
+ const data = scrollDataRef.current;
+ recordSample(scrollOffset);
+
// 设置滚动状态
- if (!isScrolling) {
+ if (!data.isScrolling) {
+ data.isScrolling = true;
setIsScrolling(true);
}
- // 计算滚动速度
- const speed = calculateSpeed(scrollOffset);
- setScrollSpeed(speed);
+ // rAF 合并状态更新,避免每次 scroll 事件触发 setState
+ data.rafId ??= requestAnimationFrame(() => {
+ data.rafId = null;
+ const speed = calculateSpeed();
+ if (Math.abs(speed - data.speed) >= speedEpsilon) {
+ data.speed = speed;
+ setScrollSpeed(speed);
+ }
+ });
// 清除之前的定时器
- if (scrollDataRef.current.timer !== null) {
- clearTimeout(scrollDataRef.current.timer);
+ if (data.timer !== null) {
+ clearTimeout(data.timer);
}
// 设置新的定时器,检测滚动停止
- scrollDataRef.current.timer = setTimeout(() => {
+ data.timer = setTimeout(() => {
+ data.isScrolling = false;
+ if (data.speed !== 0) {
+ data.speed = 0;
+ setScrollSpeed(0);
+ }
setIsScrolling(false);
- setScrollSpeed(0);
// 清空样本数据
- scrollDataRef.current.positions = [];
+ data.positions = [];
}, scrollEndDelay);
- }, [isScrolling, calculateSpeed, scrollEndDelay]);
+ }, [calculateSpeed, recordSample, scrollEndDelay, speedEpsilon]);
/**
* 检查是否为快速滚动
@@ -151,15 +174,35 @@ export function useOptimizedScroll(options: ScrollSpeedOptions = {}): {
* 重置滚动状态
*/
const reset = useCallback(() => {
+ const data = scrollDataRef.current;
setIsScrolling(false);
setScrollSpeed(0);
- scrollDataRef.current.positions = [];
+ data.isScrolling = false;
+ data.speed = 0;
+ data.positions = [];
- if (scrollDataRef.current.timer !== null) {
- clearTimeout(scrollDataRef.current.timer);
- scrollDataRef.current.timer = null;
+ if (data.timer !== null) {
+ clearTimeout(data.timer);
+ data.timer = null;
+ }
+
+ if (data.rafId !== null) {
+ cancelAnimationFrame(data.rafId);
+ data.rafId = null;
}
}, []);
+
+ useEffect(() => {
+ const data = scrollDataRef.current;
+ return () => {
+ if (data.timer !== null) {
+ clearTimeout(data.timer);
+ }
+ if (data.rafId !== null) {
+ cancelAnimationFrame(data.rafId);
+ }
+ };
+ }, []);
return {
/** 是否正在滚动 */
@@ -174,63 +217,3 @@ export function useOptimizedScroll(options: ScrollSpeedOptions = {}): {
reset
};
}
-
-/**
- * 使用RAF优化的滚动处理Hook
- *
- * 结合 requestAnimationFrame 提供更流畅的滚动体验。
- * 适用于需要高性能渲染的场景。
- *
- * @param options - 滚动配置选项
- * @returns 滚动状态和处理函数
- */
-export function useRAFOptimizedScroll(options: ScrollSpeedOptions = {}): {
- isScrolling: boolean;
- scrollSpeed: number;
- isFastScrolling: boolean;
- handleScroll: (scrollOffset: number) => void;
- reset: () => void;
-} {
- const scrollHook = useOptimizedScroll(options);
- const rafIdRef = useRef(null);
- const pendingScrollRef = useRef(null);
-
- /**
- * 使用RAF优化的滚动处理
- */
- const handleScrollRAF = useCallback((scrollOffset: number): void => {
- // 保存最新的滚动偏移
- pendingScrollRef.current = scrollOffset;
-
- // 如果已有待处理的RAF,直接返回
- if (rafIdRef.current !== null) {
- return;
- }
-
- // 请求下一帧执行
- rafIdRef.current = requestAnimationFrame(() => {
- if (pendingScrollRef.current !== null) {
- scrollHook.handleScroll(pendingScrollRef.current);
- pendingScrollRef.current = null;
- }
- rafIdRef.current = null;
- });
- }, [scrollHook]);
-
- /**
- * 清理RAF
- */
- const cleanup = useCallback(() => {
- if (rafIdRef.current !== null) {
- cancelAnimationFrame(rafIdRef.current);
- rafIdRef.current = null;
- }
- scrollHook.reset();
- }, [scrollHook]);
-
- return {
- ...scrollHook,
- handleScroll: handleScrollRAF,
- reset: cleanup
- };
-}
diff --git a/src/hooks/useThemeTransition.ts b/src/hooks/useThemeTransition.ts
index bb51252..cff6669 100644
--- a/src/hooks/useThemeTransition.ts
+++ b/src/hooks/useThemeTransition.ts
@@ -1,11 +1,12 @@
import { useEffect, useRef } from 'react';
+import type { RefObject } from 'react';
/**
* 主题切换状态 Hook
*
* 监听主题切换事件,返回可读的状态引用。
*/
-export const useThemeTransitionFlag = (): React.RefObject => {
+export const useThemeTransitionFlag = (): RefObject => {
const isThemeChangingRef = useRef(false);
useEffect(() => {
diff --git a/src/index.css b/src/index.css
index d7396cc..d2b6bc3 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,12 +1,6 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-.prose {
- max-width: 100% !important;
-}
-.prose > * {
- max-width: 100% !important;
-}
* {
transition: background-color 300ms cubic-bezier(0.05, 0.01, 0.5, 1.0),
color 300ms cubic-bezier(0.05, 0.01, 0.5, 1.0),
@@ -105,10 +99,6 @@ body.theme-transition .MuiLinearProgress-root {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
-.no-transition,
-.no-transition * {
- transition: none !important;
-}
input, select, textarea,
*:active, *:focus {
transition-duration: 0ms !important;
@@ -176,18 +166,6 @@ body {
[data-theme="dark"] ::-webkit-scrollbar-corner {
background: transparent;
}
-.pdf-page-shadow {
- box-shadow: none !important;
-}
-.pdf-page-shadow canvas {
- margin: 0;
- display: block;
- box-sizing: content-box;
- background-color: white;
-}
-.active-pdf-page {
- position: relative;
-}
[data-theme="dark"] .markdown-body hr {
border-color: rgba(208, 188, 255, 0.2) !important;
}
@@ -418,16 +396,6 @@ body {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02) !important;
}
-.virtual-file-list.locked-layout {
- scrollbar-width: none !important;
- -ms-overflow-style: none !important;
-}
-
-.virtual-file-list.locked-layout::-webkit-scrollbar {
- display: none !important;
- width: 0 !important;
- height: 0 !important;
-}
.file-list-container.no-scroll .file-list-item-container {
margin-bottom: 0 !important;
@@ -659,13 +627,6 @@ body.theme-transition .readme-container .MuiPaper-root {
animation: fileListFadeIn 0.15s ease-out;
}
-.file-list-slide-left {
- animation: slideInFromRight 0.3s ease-out forwards;
-}
-
-.file-list-slide-right {
- animation: slideInFromLeft 0.3s ease-out forwards;
-}
.file-list-item-container {
transition: transform 250ms cubic-bezier(0.1, 0.0, 0.2, 1.0),
@@ -709,10 +670,6 @@ body.theme-transition .readme-container .MuiPaper-root {
}
}
-.file-list-smooth-transition {
- transition: transform 300ms cubic-bezier(0.1, 0.0, 0.2, 1.0),
- opacity 300ms cubic-bezier(0.1, 0.0, 0.2, 1.0) !important;
-}
@keyframes fadeIn {
0% {
@@ -734,10 +691,6 @@ body.theme-transition .readme-container .MuiPaper-root {
}
}
-.fade-in {
- animation: fadeIn 0.5s ease-out 0.1s forwards;
- opacity: 0;
-}
.readme-container .MuiCircularProgress-root {
display: none !important;
diff --git a/src/services/cache/CacheStrategy.ts b/src/services/cache/CacheStrategy.ts
deleted file mode 100644
index 638c093..0000000
--- a/src/services/cache/CacheStrategy.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-/**
- * 智能缓存策略
- *
- * 实现了带有TTL、stale-while-revalidate和标签系统的智能缓存策略。
- */
-
-import { logger } from '@/utils';
-
-/**
- * 缓存配置选项
- */
-export interface CacheOptions {
- /** 生存时间(毫秒),超过此时间缓存被视为过期 */
- ttl?: number;
- /** 后台刷新时间(毫秒),在此时间内返回旧值并后台刷新 */
- staleWhileRevalidate?: number;
- /** 缓存标签,用于批量失效 */
- tags?: string[];
-}
-
-/**
- * 缓存项
- */
-interface CachedItem {
- value: T;
- timestamp: number;
- tags: string[];
-}
-
-/**
- * 智能缓存策略类
- *
- * 提供了基于时间的缓存管理和后台刷新机制。
- */
-export class SmartCacheStrategy {
- private cache = new Map();
- private revalidationQueue = new Set();
-
- /**
- * 获取缓存值,如果缓存未命中或过期则调用 fetcher 获取新值
- *
- * @param key - 缓存键
- * @param fetcher - 获取数据的函数
- * @param options - 缓存选项
- * @returns 缓存的值或新获取的值
- */
- async get(
- key: string,
- fetcher: () => Promise,
- options: CacheOptions = {}
- ): Promise {
- const cached = this.cache.get(key);
- const now = Date.now();
-
- // 缓存未命中
- if (cached === undefined) {
- logger.debug(`缓存未命中: ${key}`);
- const value = await fetcher();
- this.set(key, value, options);
- return value;
- }
-
- const age = now - cached.timestamp;
- const { ttl = 300000, staleWhileRevalidate = 600000 } = options;
-
- // 缓存新鲜,直接返回
- if (age < ttl) {
- logger.debug(`缓存命中(新鲜): ${key}, 年龄: ${age.toString()}ms`);
- return cached.value as T;
- }
-
- // 缓存过期但在后台刷新窗口内
- if (age < staleWhileRevalidate) {
- logger.debug(`缓存命中(陈旧): ${key}, 年龄: ${age.toString()}ms, 后台刷新中`);
- // 后台刷新
- if (!this.revalidationQueue.has(key)) {
- this.revalidationQueue.add(key);
- void this.revalidate(key, fetcher, options);
- }
- // 返回旧值
- return cached.value as T;
- }
-
- // 缓存完全过期,重新获取
- logger.debug(`缓存过期: ${key}, 年龄: ${age.toString()}ms`);
- const value = await fetcher();
- this.set(key, value, options);
- return value;
- }
-
- /**
- * 后台刷新缓存
- *
- * @param key - 缓存键
- * @param fetcher - 获取数据的函数
- * @param options - 缓存选项
- */
- private async revalidate(
- key: string,
- fetcher: () => Promise,
- options: CacheOptions
- ): Promise {
- try {
- const value = await fetcher();
- this.set(key, value, options);
- logger.debug(`后台刷新成功: ${key}`);
- } catch (error) {
- logger.error(`后台刷新失败: ${key}`, error);
- } finally {
- this.revalidationQueue.delete(key);
- }
- }
-
- /**
- * 设置缓存值
- *
- * @param key - 缓存键
- * @param value - 缓存值
- * @param options - 缓存选项
- */
- private set(key: string, value: unknown, options: CacheOptions): void {
- this.cache.set(key, {
- value,
- timestamp: Date.now(),
- tags: options.tags ?? [],
- });
- }
-
- /**
- * 按标签失效缓存
- *
- * @param tag - 缓存标签
- * @returns 失效的缓存项数量
- */
- invalidateByTag(tag: string): number {
- let count = 0;
- for (const [key, item] of this.cache.entries()) {
- if (item.tags.includes(tag)) {
- this.cache.delete(key);
- count++;
- }
- }
- if (count > 0) {
- logger.debug(`按标签失效缓存: ${tag}, 失效 ${count.toString()} 项`);
- }
- return count;
- }
-
- /**
- * 删除指定的缓存项
- *
- * @param key - 缓存键
- * @returns 是否成功删除
- */
- delete(key: string): boolean {
- return this.cache.delete(key);
- }
-
- /**
- * 清空所有缓存
- */
- clear(): void {
- this.cache.clear();
- this.revalidationQueue.clear();
- logger.debug('清空所有缓存');
- }
-
- /**
- * 获取缓存统计信息
- *
- * @returns 缓存统计信息
- */
- getStats(): {
- size: number;
- revalidating: number;
- keys: string[];
- } {
- return {
- size: this.cache.size,
- revalidating: this.revalidationQueue.size,
- keys: Array.from(this.cache.keys()),
- };
- }
-
- /**
- * 检查缓存是否存在且新鲜
- *
- * @param key - 缓存键
- * @param ttl - 生存时间(毫秒)
- * @returns 是否存在且新鲜
- */
- isFresh(key: string, ttl = 300000): boolean {
- const cached = this.cache.get(key);
- if (cached === undefined) {
- return false;
- }
- const age = Date.now() - cached.timestamp;
- return age < ttl;
- }
-}
-
-/**
- * 全局智能缓存策略实例
- */
-export const cacheStrategy = new SmartCacheStrategy();
-
diff --git a/src/services/configService.ts b/src/services/configService.ts
deleted file mode 100644
index 52922bc..0000000
--- a/src/services/configService.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-import { logger } from '../utils';
-import { getSiteConfig, getGithubConfig } from '../config';
-
-/**
- * 配置信息接口
- */
-export interface ConfigInfo {
- /** 网站标题 */
- siteTitle: string;
- /** 仓库所有者 */
- repoOwner: string;
- /** 仓库名称 */
- repoName: string;
- /** 仓库分支 */
- repoBranch: string;
-}
-
-/**
- * 配置API成功响应接口
- */
-interface ConfigApiSuccessResponse {
- status: 'success';
- data?: unknown;
-}
-
-/**
- * 配置默认值
- */
-const DEFAULT_CONFIG: ConfigInfo = {
- siteTitle: getSiteConfig().title,
- repoOwner: getGithubConfig().repoOwner,
- repoName: getGithubConfig().repoName,
- repoBranch: getGithubConfig().repoBranch
-};
-
-// 存储当前配置
-let currentConfig: ConfigInfo = { ...DEFAULT_CONFIG };
-
-// 初始化状态
-let isInitialized = false;
-let initPromise: Promise | null = null;
-
-const isConfigApiSuccessResponse = (value: unknown): value is ConfigApiSuccessResponse => {
- if (typeof value !== 'object' || value === null) {
- return false;
- }
-
- return (value as { status?: unknown }).status === 'success';
-};
-
-const normalizeConfigData = (data: unknown): Partial => {
- if (typeof data !== 'object' || data === null) {
- return {};
- }
-
- const record = data as Record;
- const normalized: Partial = {};
-
- const siteTitle = record['siteTitle'];
- if (typeof siteTitle === 'string') {
- normalized.siteTitle = siteTitle;
- }
-
- const repoOwner = record['repoOwner'];
- if (typeof repoOwner === 'string') {
- normalized.repoOwner = repoOwner;
- }
-
- const repoName = record['repoName'];
- if (typeof repoName === 'string') {
- normalized.repoName = repoName;
- }
-
- const repoBranch = record['repoBranch'];
- if (typeof repoBranch === 'string') {
- normalized.repoBranch = repoBranch;
- }
-
- return normalized;
-};
-
-const updateDocumentTitle = (siteTitle: string): void => {
- if (typeof document === 'undefined') {
- return;
- }
-
- const trimmedTitle = siteTitle.trim();
-
- if (trimmedTitle.length === 0) {
- return;
- }
-
- document.title = trimmedTitle;
-};
-
-const loadConfig = async (): Promise => {
- try {
- logger.debug('正在从API加载配置信息...');
-
- const headers: HeadersInit = {
- Accept: 'application/json',
- 'Cache-Control': 'no-cache',
- Pragma: 'no-cache'
- };
-
- const timestamp = Date.now().toString();
- const response = await fetch(`/api/github?action=getConfig&t=${encodeURIComponent(timestamp)}`, { headers });
-
- if (!response.ok) {
- throw new Error(`API请求失败: ${response.status.toString()}`);
- }
-
- const contentType = response.headers.get('content-type');
-
- if (typeof contentType !== 'string' || !contentType.includes('application/json')) {
- logger.warn(`API响应格式不是JSON: ${contentType ?? 'unknown'}`);
- const text = await response.text();
- logger.debug('API原始响应:', text);
- throw new Error('API返回格式不正确');
- }
-
- const result: unknown = await response.json();
-
- if (isConfigApiSuccessResponse(result)) {
- const normalizedConfig = normalizeConfigData(result.data);
- currentConfig = {
- ...DEFAULT_CONFIG,
- ...normalizedConfig
- };
-
- logger.debug('配置信息加载成功', currentConfig);
- return currentConfig;
- }
-
- logger.warn('API返回无效配置数据,使用默认值');
- } catch (error: unknown) {
- logger.error('加载配置失败,使用默认配置', error);
- }
-
- currentConfig = { ...DEFAULT_CONFIG };
- return currentConfig;
-};
-
-/**
- * 获取当前配置信息
- *
- * @returns 当前的配置信息对象
- */
-const getConfig = (): ConfigInfo => currentConfig;
-
-/**
- * 获取网站标题
- *
- * @returns 当前网站标题
- */
-const getSiteTitle = (): string => currentConfig.siteTitle;
-
-/**
- * 检查配置服务是否已初始化
- *
- * @returns 如果已初始化返回true,否则返回false
- */
-const isServiceInitialized = (): boolean => isInitialized;
-
-/**
- * 初始化配置服务
- *
- * 从API加载配置信息并更新文档标题。
- * 使用 Promise 缓存防止竞态条件,确保配置只加载一次。
- *
- * @returns Promise,解析为配置信息对象
- */
-const init = (): Promise => {
- // 已初始化,直接返回当前配置
- if (isInitialized) {
- return Promise.resolve(currentConfig);
- }
-
- // 正在初始化,返回缓存的 Promise
- if (initPromise !== null) {
- return initPromise;
- }
-
- // 先设置 initPromise,防止竞态条件
- initPromise = loadConfig()
- .then(config => {
- isInitialized = true;
- updateDocumentTitle(config.siteTitle);
- return config;
- })
- .catch((error: unknown) => {
- logger.error('配置初始化失败', error);
- // 失败时重置 initPromise,允许重试
- initPromise = null;
- return currentConfig;
- });
-
- return initPromise;
-};
-
-/**
- * 配置服务API接口
- */
-interface ConfigServiceApi {
- /** 初始化配置服务 */
- init: () => Promise;
- /** 重新加载配置 */
- loadConfig: () => Promise;
- /** 获取当前配置 */
- getConfig: () => ConfigInfo;
- /** 获取网站标题 */
- getSiteTitle: () => string;
- /** 检查是否已初始化 */
- isInitialized: () => boolean;
-}
-
-/**
- * 配置服务对象
- *
- * 提供配置管理功能,包括从API加载配置、获取配置信息和检查初始化状态。
- */
-export const ConfigService: ConfigServiceApi = {
- init,
- loadConfig,
- getConfig,
- getSiteTitle,
- isInitialized: isServiceInitialized
-};
-
-export default ConfigService;
diff --git a/src/services/github.ts b/src/services/github.ts
index f0d9d85..52a0511 100644
--- a/src/services/github.ts
+++ b/src/services/github.ts
@@ -13,7 +13,6 @@ import * as StatsServiceModule from './github/core/StatsService';
import * as PrefetchServiceModule from './github/core/PrefetchService';
import * as AuthModule from './github/core/Auth';
import * as ConfigModule from './github/core/Config';
-import { getDefaultBranchName } from './github/core/Service';
import { CacheManager as CacheManagerClass } from './github/cache';
import { GitHubTokenManager } from './github/TokenManager';
import {
@@ -72,7 +71,7 @@ export const GitHub = {
getBranches: BranchServiceModule.getBranches,
getCurrentBranch: ConfigModule.getCurrentBranch,
setCurrentBranch: ConfigModule.setCurrentBranch,
- getDefaultBranchName: getDefaultBranchName,
+ getDefaultBranchName: ConfigModule.getDefaultBranch,
},
/** 缓存服务 - 管理缓存和统计 */
@@ -98,7 +97,6 @@ export const GitHub = {
getAuthHeaders: AuthModule.getAuthHeaders,
handleApiError: AuthModule.handleApiError,
updateTokenRateLimitFromResponse: AuthModule.updateTokenRateLimitFromResponse,
- getTokenManager: AuthModule.getTokenManager,
},
/** 代理服务 - 管理代理和图片转换 */
diff --git a/src/services/github/PatService.ts b/src/services/github/PatService.ts
deleted file mode 100644
index d003e8e..0000000
--- a/src/services/github/PatService.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-import { configManager } from '@/config';
-import { GitHubTokenManager } from './TokenManager';
-
-/**
- * GitHub PAT服务类
- *
- * 提供统一的Personal Access Token获取和管理接口。
- * 支持token轮换、失败处理和调试信息。
- */
-export class PatService {
- private static instance: PatService | null = null;
- private tokenManager: GitHubTokenManager;
-
- private constructor() {
- this.tokenManager = new GitHubTokenManager();
- }
-
- /**
- * 获取PatService单例实例
- *
- * @returns PatService实例
- */
- static getInstance(): PatService {
- PatService.instance ??= new PatService();
- return PatService.instance;
- }
-
- /**
- * 获取当前可用的GitHub PAT
- *
- * @returns GitHub PAT字符串,如果没有可用token则返回空字符串
- */
- getCurrentPAT(): string {
- return this.tokenManager.getCurrentToken();
- }
-
- /**
- * 获取下一个可用的GitHub PAT
- *
- * 用于token轮换。
- *
- * @returns GitHub PAT字符串
- */
- getNextPAT(): string {
- return this.tokenManager.getNextToken();
- }
-
- /**
- * 获取GitHub PAT并标记使用
- *
- * 推荐使用此方法,会自动记录使用次数。
- *
- * @returns GitHub PAT字符串
- */
- getGitHubPAT(): string {
- return this.tokenManager.getGitHubPAT();
- }
-
- /**
- * 检查是否有可用的PAT
- *
- * @returns 如果有可用PAT返回true
- */
- hasTokens(): boolean {
- return this.tokenManager.hasTokens();
- }
-
- /**
- * 获取可用PAT数量
- *
- * @returns PAT数量
- */
- getTokenCount(): number {
- return this.tokenManager.getTokenCount();
- }
-
- /**
- * 标记当前PAT为失败状态
- *
- * @returns void
- */
- markCurrentTokenFailed(): void {
- this.tokenManager.markCurrentTokenFailed();
- }
-
- /**
- * 处理API错误
- *
- * 自动处理token轮换。
- *
- * @param error - API响应错误对象
- * @returns void
- */
- handleApiError(error: Response): void {
- this.tokenManager.handleApiError(error);
- }
-
- /**
- * 设置本地PAT
- *
- * 主要用于开发环境。
- *
- * @param token - PAT字符串
- * @returns void
- */
- setLocalToken(token: string): void {
- this.tokenManager.setLocalToken(token);
- }
-
- /**
- * 重新加载环境变量中的PAT
- *
- * @returns void
- */
- reloadTokens(): void {
- this.tokenManager.loadTokensFromEnv();
- }
-
- /**
- * 获取PAT配置的调试信息
- *
- * @returns 包含token数量和来源的调试信息对象
- */
- getDebugInfo(): { totalTokens: number; tokenSources: unknown } {
- const debugInfo = configManager.getDebugInfo();
- return {
- totalTokens: debugInfo.configSummary.tokenCount,
- tokenSources: debugInfo.tokenSources
- };
- }
-
- /**
- * 获取推荐的PAT配置格式
- *
- * @returns 推荐配置示例对象
- */
- getRecommendedConfig(): Record {
- return {
- 'VITE_GITHUB_PAT1': 'your_primary_token_here',
- 'VITE_GITHUB_PAT2': 'your_secondary_token_here',
- 'VITE_GITHUB_PAT3': 'your_tertiary_token_here'
- };
- }
-
- /**
- * 重置单例实例
- *
- * 主要用于测试。
- *
- * @returns void
- */
- static resetInstance(): void {
- PatService.instance = null;
- }
-}
-
-/**
- * PAT服务单例实例
- *
- * 全局PAT服务实例。
- */
-export const patService = PatService.getInstance();
-
-/**
- * 获取GitHub PAT的便捷函数
- *
- * @returns GitHub PAT字符串
- */
-export const getGitHubPAT = (): string => patService.getGitHubPAT();
-
-/**
- * 检查是否有GitHub Token的便捷函数
- *
- * @returns 如果有可用token返回true
- */
-export const hasGitHubTokens = (): boolean => patService.hasTokens();
-
-/**
- * 标记token失败的便捷函数
- *
- * @returns void
- */
-export const markTokenFailed = (): void => {
- patService.markCurrentTokenFailed();
-};
diff --git a/src/services/github/RequestBatcher.ts b/src/services/github/RequestBatcher.ts
index 68114b2..4a9b24f 100644
--- a/src/services/github/RequestBatcher.ts
+++ b/src/services/github/RequestBatcher.ts
@@ -1,5 +1,4 @@
-import { logger, retry } from '@/utils';
-import type { RetryOptions } from '@/utils';
+import { logger } from '@/utils';
import { createTimeWheel } from '@/utils/data-structures/TimeWheel';
import type { TimeWheel } from '@/utils/data-structures/TimeWheel';
@@ -22,11 +21,22 @@ interface FingerprintData {
hitCount: number;
}
+/**
+ * 重试选项
+ */
+interface RetryOptions {
+ maxRetries: number;
+ backoff: (attempt: number) => number;
+ shouldRetry?: (error: unknown) => boolean;
+ onRetry?: (attempt: number, error: unknown) => void;
+ silent?: boolean;
+}
+
/**
* 请求批处理器类
*
* 管理和优化HTTP请求,提供请求合并、去重、优先级排序和重试机制。
- * 自动批处理相同的请求,减少网络开销并提升性能。
+ * 自动批处理重复请求,减少网络开销并提升性能。
*/
export class RequestBatcher {
private readonly batchedRequests = new Map();
@@ -61,21 +71,53 @@ export class RequestBatcher {
}
/**
- * 销毁批处理器
- *
- * 清理所有定时器和缓存,释放资源。
- *
- * @returns void
+ * 重试选项
*/
- public destroy(): void {
- if (this.batchTimeout !== null && this.batchTimeout !== 0 && !isNaN(this.batchTimeout)) {
- clearTimeout(this.batchTimeout);
+ private withRetry = async (
+ fn: () => Promise,
+ options: RetryOptions
+ ): Promise => {
+ const {
+ maxRetries,
+ backoff,
+ shouldRetry = () => true,
+ onRetry,
+ silent = false
+ } = options;
+
+ let lastError: unknown = null;
+
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
+ try {
+ const result = await fn();
+ if (attempt > 0) {
+ logger.debug(`操作在第 ${(attempt + 1).toString()} 次尝试后成功`);
+ }
+ return result;
+ } catch (error: unknown) {
+ lastError = error;
+
+ if (attempt < maxRetries && shouldRetry(error)) {
+ const delay = backoff(attempt);
+
+ if (onRetry !== undefined) {
+ onRetry(attempt, error);
+ }
+
+ if (!silent) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ logger.debug(`重试操作 (尝试 ${(attempt + 1).toString()}/${(maxRetries + 1).toString()}),延迟 ${delay.toString()}ms: ${errorMessage}`);
+ }
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+ } else {
+ break;
+ }
+ }
}
- this.batchedRequests.clear();
- this.pendingRequests.clear();
- this.fingerprintWheel.destroy();
- logger.debug('RequestBatcher 已销毁');
- }
+
+ throw lastError;
+ };
/**
* 将请求加入批处理队列
@@ -108,7 +150,7 @@ export class RequestBatcher {
skipDeduplication = false
} = options;
- // 检查是否有相同的请求正在进行
+ // 检查是否有重复请求正在进行
if (this.pendingRequests.has(key)) {
logger.debug(`请求合并: ${key}`);
return this.pendingRequests.get(key) as Promise;
@@ -208,9 +250,9 @@ export class RequestBatcher {
try {
// 使用通用重试逻辑执行请求
- const result = await retry.withRetry(executeRequest, retryOptions);
+ const result = await this.withRetry(executeRequest, retryOptions);
- // 缓存成功的请求结果
+ // 缓存成功响应的结果
if (!skipDeduplication) {
this.fingerprintWheel.add(fingerprint, {
result,
@@ -278,21 +320,4 @@ export class RequestBatcher {
logger.debug('已清除请求指纹缓存');
}
- /**
- * 强制取消所有等待的请求
- *
- * 取消所有在队列中等待的请求,清空批处理队列。
- *
- * @returns void
- */
- public cancelAllRequests(): void {
- for (const [, queue] of this.batchedRequests.entries()) {
- queue.forEach(request => {
- request.reject(new Error('请求被取消'));
- });
- }
- this.batchedRequests.clear();
- this.pendingRequests.clear();
- logger.debug('已取消所有等待的请求');
- }
}
diff --git a/src/services/github/TokenManager.ts b/src/services/github/TokenManager.ts
index 35ac396..e17e751 100644
--- a/src/services/github/TokenManager.ts
+++ b/src/services/github/TokenManager.ts
@@ -203,29 +203,6 @@ export class GitHubTokenManager {
return this.tokens.length;
}
- /**
- * 轮换到下一个Token
- *
- * @returns 下一个可用的Token字符串
- */
- public rotateToNextToken(): string {
- return this.getNextToken();
- }
-
- /**
- * 标记当前Token失败
- *
- * 将当前正在使用的token标记为失败状态。
- *
- * @returns void
- */
- public markCurrentTokenFailed(): void {
- const currentToken = this.getCurrentToken();
- if (currentToken !== '') {
- this.markTokenFailed(currentToken);
- }
- }
-
/**
* 设置本地Token
*
@@ -431,32 +408,5 @@ export class GitHubTokenManager {
}
}
- /**
- * 获取 Token 状态统计
- *
- * @returns Token 状态信息数组
- */
- public getTokenStats(): {
- index: number;
- hasState: boolean;
- rateLimitRemaining: number;
- failureCount: number;
- inBackoff: boolean;
- }[] {
- const now = Date.now();
- return this.tokens.map((token, index) => {
- const state = this.tokenStates.get(token);
- const inBackoff = state !== undefined &&
- state.failureCount > 0 &&
- (now - state.lastFailure) < GitHubTokenManager.BACKOFF_DURATION;
-
- return {
- index,
- hasState: state !== undefined,
- rateLimitRemaining: state?.rateLimitRemaining ?? -1,
- failureCount: state?.failureCount ?? 0,
- inBackoff
- };
- });
- }
+
}
diff --git a/src/services/github/cache/AdvancedCache.ts b/src/services/github/cache/AdvancedCache.ts
index 5524acf..aedf6d0 100644
--- a/src/services/github/cache/AdvancedCache.ts
+++ b/src/services/github/cache/AdvancedCache.ts
@@ -31,7 +31,6 @@ export class AdvancedCache {
private readonly cache: LRUCache;
private readonly config: CacheConfig;
private readonly stats: CacheStats;
- private cleanupTimer: ReturnType | null = null;
private readonly dbName: string;
private db: IDBDatabase | null = null;
@@ -328,7 +327,7 @@ export class AdvancedCache {
const scheduleNextCleanup = (): void => {
const interval = getCleanupInterval();
- this.cleanupTimer = setTimeout(() => {
+ setTimeout(() => {
this.performPeriodicCleanup()
.then(() => {
// 清理完成后,根据新的缓存大小重新安排下次清理
@@ -389,25 +388,6 @@ export class AdvancedCache {
}, this.config.prefetchDelay);
}
- /**
- * 销毁缓存实例
- *
- * 清理定时器和数据库连接。
- *
- * @returns void
- */
- destroy(): void {
- if (this.cleanupTimer !== null) {
- clearTimeout(this.cleanupTimer);
- this.cleanupTimer = null;
- }
- if (this.db !== null) {
- this.db.close();
- this.db = null;
- }
- this.cache.clear();
- }
-
private async loadItemFromPersistence(key: string): Promise {
if (this.config.useIndexedDB && this.db !== null) {
return loadItemFromIndexedDB(this.db, this.config, key);
diff --git a/src/services/github/cache/CacheManager.ts b/src/services/github/cache/CacheManager.ts
index 2b11b2f..b94e65e 100644
--- a/src/services/github/cache/CacheManager.ts
+++ b/src/services/github/cache/CacheManager.ts
@@ -139,39 +139,6 @@ class CacheManagerImpl {
this.contentCache.prefetch(keys);
}
-
- /**
- * 预取文件
- *
- * 预加载指定URL的文件到缓存中。
- *
- * @param urls - 要预取的URL数组
- * @returns void
- */
- public prefetchFiles(urls: string[]): void {
- if (this.fileCache !== null) {
- this.fileCache.prefetch(urls);
- }
- }
-
- /**
- * 销毁缓存管理器
- *
- * 清理所有缓存资源并重置初始化状态。
- *
- * @returns void
- */
- public destroy(): void {
- if (this.contentCache !== null) {
- this.contentCache.destroy();
- }
- if (this.fileCache !== null) {
- this.fileCache.destroy();
- }
-
- this.initialized = false;
- logger.info('缓存管理器已销毁');
- }
}
/**
diff --git a/src/services/github/cache/LRUCache.ts b/src/services/github/cache/LRUCache.ts
index ae728a7..3270b0f 100644
--- a/src/services/github/cache/LRUCache.ts
+++ b/src/services/github/cache/LRUCache.ts
@@ -146,26 +146,6 @@ export class LRUCache {
}
}
- /**
- * 获取最少使用的 N 个条目
- *
- * @param count - 要获取的条目数量
- * @returns 最少使用的条目键数组
- */
- getLeastUsed(count: number): K[] {
- const result: K[] = [];
- let current = this.head;
- let collected = 0;
-
- while (current !== null && collected < count) {
- result.push(current.key);
- current = current.next;
- collected++;
- }
-
- return result;
- }
-
/**
* 移动节点到尾部(标记为最近使用)
*
@@ -238,4 +218,3 @@ export class LRUCache {
logger.debug(`LRU缓存已满,删除最久未使用的项: ${lruKey}`);
}
}
-
diff --git a/src/services/github/config/ProxyForceManager.ts b/src/services/github/config/ProxyForceManager.ts
index d93b252..ed2c7c9 100644
--- a/src/services/github/config/ProxyForceManager.ts
+++ b/src/services/github/config/ProxyForceManager.ts
@@ -119,9 +119,6 @@ export function getRequestStrategy(): 'server-api' | 'direct-api' | 'hybrid' {
}
// 导出便捷别名函数
-export const getForceServerProxyAlias = (): boolean => getForceServerProxy();
-export const shouldUseServerAPIAlias = (): boolean => shouldUseServerAPI();
-export const getRequestStrategyAlias = (): 'server-api' | 'direct-api' | 'hybrid' => getRequestStrategy();
export const refreshProxyConfig = (): void => {
refreshConfig();
};
diff --git a/src/services/github/config/index.ts b/src/services/github/config/index.ts
index 64457a5..33acd85 100644
--- a/src/services/github/config/index.ts
+++ b/src/services/github/config/index.ts
@@ -6,12 +6,3 @@ export {
getProxyConfigDetails,
refreshProxyConfig
} from './ProxyForceManager';
-
-export type ProxyStrategy = 'server-api' | 'direct-api' | 'hybrid';
-
-export interface ProxyConfigDetails {
- forceServerProxy: boolean;
- isDev: boolean;
- useTokenMode: boolean;
- reason: string;
-}
diff --git a/src/services/github/core/Auth.ts b/src/services/github/core/Auth.ts
index ab6fe20..90578bc 100644
--- a/src/services/github/core/Auth.ts
+++ b/src/services/github/core/Auth.ts
@@ -1,10 +1,4 @@
import { GitHubTokenManager } from '../TokenManager';
-import {
- markProxyServiceFailed as proxyMarkServiceFailed,
- getCurrentProxyService as proxyGetCurrentService,
- resetFailedProxyServices as proxyResetFailedServices,
- transformImageUrl as proxyTransformImageUrl
-} from '../proxy';
import { ErrorManager } from '@/utils/error';
import type { GitHubError } from '@/types/errors';
import { shouldUseServerAPI } from '../config';
@@ -138,60 +132,3 @@ export function handleApiError(error: Response, endpoint: string, method = 'GET'
return gitHubError;
}
-
-/**
- * 标记代理服务失败
- *
- * 将指定的代理服务标记为失败状态,触发代理服务切换。
- *
- * @param proxyUrl - 失败的代理服务URL
- * @returns void
- */
-export function markProxyServiceFailed(proxyUrl: string): void {
- proxyMarkServiceFailed(proxyUrl);
-}
-
-/**
- * 获取当前使用的代理服务
- *
- * @returns 当前活跃的代理服务URL
- */
-export function getCurrentProxyService(): string {
- return proxyGetCurrentService();
-}
-
-/**
- * 重置失败的代理服务记录
- *
- * 清除所有代理服务的失败标记,允许重新尝试使用。
- *
- * @returns void
- */
-export function resetFailedProxyServices(): void {
- proxyResetFailedServices();
-}
-
-/**
- * 转换相对图片URL为绝对URL
- *
- * 将Markdown文件中的相对路径图片URL转换为可访问的绝对URL,
- * 根据useTokenMode决定是否使用代理服务。
- *
- * @param src - 原始图片URL(可能是相对路径)
- * @param markdownFilePath - Markdown文件的路径
- * @param useTokenMode - 是否使用Token模式
- * @param branch - 分支名称(可选)
- * @returns 转换后的绝对URL,如果输入为undefined则返回undefined
- */
-export function transformImageUrl(src: string | undefined, markdownFilePath: string, useTokenMode: boolean, branch?: string): string | undefined {
- return proxyTransformImageUrl(src, markdownFilePath, useTokenMode, branch);
-}
-
-/**
- * 获取 Token 管理器实例
- *
- * @returns TokenManager 实例
- */
-export function getTokenManager(): GitHubTokenManager {
- return tokenManager;
-}
diff --git a/src/services/github/core/BranchService.ts b/src/services/github/core/BranchService.ts
index 1dcc188..a95469f 100644
--- a/src/services/github/core/BranchService.ts
+++ b/src/services/github/core/BranchService.ts
@@ -183,6 +183,4 @@ export async function getBranches(): Promise {
}
}
-export default {
- getBranches
-};
+
diff --git a/src/services/github/core/Config.ts b/src/services/github/core/Config.ts
index a01d75a..bb971b2 100644
--- a/src/services/github/core/Config.ts
+++ b/src/services/github/core/Config.ts
@@ -92,13 +92,13 @@ export function getApiUrl(path: string, branch?: string): string {
const branchValue = branch ?? currentBranch;
const activeBranch = branchValue.trim() !== '' ? branchValue.trim() : DEFAULT_BRANCH;
const encodedBranch = encodeURIComponent(activeBranch);
- const apiUrl = `https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/contents/${safePath}?ref=${encodedBranch}`;
+ const encodedPath = safePath.length > 0
+ ? safePath.split('/').map(segment => encodeURIComponent(segment)).join('/')
+ : '';
+ const apiUrl = `https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/contents/${encodedPath}?ref=${encodedBranch}`;
// 开发环境使用本地代理
if (isDevEnvironment) {
- const encodedPath = safePath.length > 0
- ? safePath.split('/').map(segment => encodeURIComponent(segment)).join('/')
- : '';
return `/github-api/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/contents/${encodedPath}?ref=${encodedBranch}`;
}
diff --git a/src/services/github/core/Service.ts b/src/services/github/core/Service.ts
deleted file mode 100644
index da553c9..0000000
--- a/src/services/github/core/Service.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-import type { GitHubContent } from '@/types';
-import {
- getTokenCount as authGetTokenCount,
- hasToken as authHasToken,
- setLocalToken as authSetLocalToken,
- markProxyServiceFailed as authMarkProxyServiceFailed,
- getCurrentProxyService as authGetCurrentProxyService,
- resetFailedProxyServices as authResetFailedProxyServices,
- transformImageUrl as authTransformImageUrl
-} from './Auth';
-import {
- searchWithGitHubApi as searchWithApi,
- searchFiles as searchFilesImpl
-} from './search';
-import {
- prefetchContents as prefetchContentsImpl,
- batchPrefetchContents as batchPrefetchContentsImpl,
- prefetchRelatedContent as prefetchRelatedContentImpl
-} from './PrefetchService';
-import {
- clearCache as statsClearCache,
- getCacheStats as statsGetCacheStats,
- getNetworkStats as statsGetNetworkStats
-} from './StatsService';
-import {
- getConfig,
- getCurrentBranch as getActiveBranch,
- setCurrentBranch as setActiveBranch,
- getDefaultBranch,
- type ConfigInfo
-} from './Config';
-import { getBranches as fetchBranches } from './BranchService';
-
-/**
- * 获取GitHub PAT总数
- *
- * @returns 已配置的GitHub Personal Access Token数量
- */
-export function getTokenCount(): number {
- return authGetTokenCount();
-}
-
-/**
- * 检查是否配置了有效token
- *
- * @returns 如果至少配置了一个有效token则返回true
- */
-export function hasToken(): boolean {
- return authHasToken();
-}
-
-/**
- * 设置本地Token
- *
- * 在localStorage中存储GitHub PAT,主要用于开发环境测试。
- *
- * @param token - GitHub Personal Access Token
- * @returns void
- */
-export function setLocalToken(token: string): void {
- authSetLocalToken(token);
-}
-
-/**
- * 获取GitHub配置信息
- *
- * @returns GitHub仓库配置对象
- */
-export function getGitHubConfig(): ConfigInfo {
- return getConfig();
-}
-
-/**
- * 获取当前活动分支名称
- *
- * @returns 当前分支名称
- */
-export function getCurrentBranch(): string {
- return getActiveBranch();
-}
-
-/**
- * 设置当前活动分支
- *
- * @param branch - 要切换到的分支名称
- * @returns void
- */
-export function setCurrentBranch(branch: string): void {
- setActiveBranch(branch);
-}
-
-/**
- * 获取默认分支名称
- *
- * @returns 默认分支名称
- */
-export function getDefaultBranchName(): string {
- return getDefaultBranch();
-}
-
-/**
- * 获取仓库的所有分支
- *
- * @returns Promise,解析为分支名称数组
- */
-export async function getBranches(): Promise {
- return fetchBranches();
-}
-
-/**
- * 获取目录内容
- *
- * 获取指定路径的目录内容,并自动触发相关内容的预加载。
- *
- * @param path - 目录路径
- * @param signal - 可选的中断信号
- * @returns Promise,解析为GitHub内容数组
- */
-export async function getContents(path: string, signal?: AbortSignal): Promise {
- const { getContents: getContentsImpl } = await import('./content');
- const contents = await getContentsImpl(path, signal);
-
- // 预加载相关内容
- void prefetchRelatedContentImpl(contents).catch(() => {
- // 忽略预加载错误
- });
-
- return contents;
-}
-
-/**
- * 获取文件内容
- *
- * @param fileUrl - 文件的URL地址
- * @returns Promise,解析为文件的文本内容
- */
-export async function getFileContent(fileUrl: string): Promise {
- const { getFileContent: getFileContentImpl } = await import('./content');
- return getFileContentImpl(fileUrl);
-}
-
-/**
- * 使用GitHub API进行代码搜索
- *
- * @param searchTerm - 搜索关键词
- * @param currentPath - 限制搜索的路径范围,默认为空
- * @param fileTypeFilter - 文件扩展名过滤器
- * @returns Promise,解析为匹配的GitHub内容数组
- */
-export async function searchWithGitHubApi(
- searchTerm: string,
- currentPath = '',
- fileTypeFilter?: string
-): Promise {
- return searchWithApi(searchTerm, currentPath, fileTypeFilter);
-}
-
-/**
- * 使用 Trees API 进行多分支搜索
- *
- * @param searchTerm - 搜索关键词
- * @param branches - 要搜索的分支列表
- * @param pathPrefix - 路径前缀
- * @param fileTypeFilter - 文件扩展名过滤器
- * @returns Promise,解析为所有分支的匹配结果
- */
-export async function searchMultipleBranchesWithTreesApi(
- searchTerm: string,
- branches: string[],
- pathPrefix = '',
- fileTypeFilter?: string
-): Promise<{ branch: string; results: GitHubContent[] }[]> {
- const { searchMultipleBranchesWithTreesApi: impl } = await import('./search');
- return impl(searchTerm, branches, pathPrefix, fileTypeFilter);
-}
-
-/**
- * 搜索文件
- *
- * @param searchTerm - 搜索关键词
- * @param currentPath - 起始搜索路径
- * @param recursive - 是否递归搜索子目录
- * @param fileTypeFilter - 文件扩展名过滤器
- * @returns Promise,解析为匹配的文件数组
- */
-export async function searchFiles(
- searchTerm: string,
- currentPath = '',
- recursive = false,
- fileTypeFilter?: string
-): Promise {
- return searchFilesImpl(searchTerm, currentPath, recursive, fileTypeFilter);
-}
-
-/**
- * 智能预取目录内容
- *
- * @param path - 要预取的目录路径
- * @param priority - 预取优先级,默认为'low'
- * @returns void
- */
-export function prefetchContents(path: string, priority: 'high' | 'medium' | 'low' = 'low'): void {
- prefetchContentsImpl(path, priority);
-}
-
-/**
- * 批量预加载多个路径
- *
- * @param paths - 要预加载的路径数组
- * @param maxConcurrency - 最大并发数,默认为3
- * @returns Promise,所有预加载完成后解析
- */
-export async function batchPrefetchContents(paths: string[], maxConcurrency = 3): Promise {
- return batchPrefetchContentsImpl(paths, maxConcurrency);
-}
-
-/**
- * 标记代理服务失败
- *
- * @param proxyUrl - 失败的代理服务URL
- * @returns void
- */
-export function markProxyServiceFailed(proxyUrl: string): void {
- authMarkProxyServiceFailed(proxyUrl);
-}
-
-/**
- * 获取当前使用的代理服务
- *
- * @returns 当前活跃的代理服务URL
- */
-export function getCurrentProxyService(): string {
- return authGetCurrentProxyService();
-}
-
-/**
- * 重置失败的代理服务记录
- *
- * 清除所有代理服务的失败标记。
- *
- * @returns void
- */
-export function resetFailedProxyServices(): void {
- authResetFailedProxyServices();
-}
-
-/**
- * 转换相对图片URL为绝对URL
- *
- * @param src - 原始图片URL
- * @param markdownFilePath - Markdown文件的路径
- * @param useTokenMode - 是否使用Token模式
- * @param branch - 分支名称(可选)
- * @returns 转换后的绝对URL
- */
-export function transformImageUrl(src: string | undefined, markdownFilePath: string, useTokenMode: boolean, branch?: string): string | undefined {
- return authTransformImageUrl(src, markdownFilePath, useTokenMode, branch);
-}
-
-/**
- * 清除所有缓存和重置网络状态
- *
- * @returns Promise,清除完成后解析
- */
-export async function clearCache(): Promise {
- return statsClearCache();
-}
-
-/**
- * 获取缓存统计信息
- *
- * @returns 缓存统计对象
- */
-export function getCacheStats(): ReturnType {
- return statsGetCacheStats();
-}
-
-/**
- * 获取网络请求统计信息
- *
- * @returns Promise,解析为网络统计对象
- */
-export async function getNetworkStats(): Promise> {
- return statsGetNetworkStats();
-}
-
-/**
- * 获取请求批处理器实例
- *
- * 主要用于调试目的。
- *
- * @returns Promise,解析为批处理器实例
- */
-export async function getBatcher(): Promise {
- const { getBatcher: getBatcherImpl } = await import('./content');
- return getBatcherImpl();
-}
-
-export type { ConfigInfo } from './Config';
diff --git a/src/services/github/core/StatsService.ts b/src/services/github/core/StatsService.ts
index 6721963..31576c0 100644
--- a/src/services/github/core/StatsService.ts
+++ b/src/services/github/core/StatsService.ts
@@ -1,6 +1,5 @@
import { CacheManager } from '../cache';
-import { getProxyHealthStats } from '../proxy';
-import { resetFailedProxyServices } from './Auth';
+import { getProxyHealthStats, resetFailedProxyServices } from '../proxy';
// GitHub统计服务,使用模块导出而非类
@@ -46,4 +45,3 @@ export async function getNetworkStats(): Promise<{
cache: getCacheStats()
};
}
-
diff --git a/src/services/github/core/content/cacheState.ts b/src/services/github/core/content/cacheState.ts
index 2a86c43..c1288f3 100644
--- a/src/services/github/core/content/cacheState.ts
+++ b/src/services/github/core/content/cacheState.ts
@@ -154,21 +154,3 @@ export async function storeFileContent(cacheKey: string, fileUrl: string, conten
fallbackCache.set(cacheKey, content);
}
-
-/**
- * 移除文件内容缓存。
- *
- * 用于在注水数据过期时清除对应的缓存条目,确保后续请求从 API 获取最新内容。
- *
- * @param cacheKey - 文件缓存键
- * @returns Promise
- */
-export async function removeCachedFileContent(cacheKey: string): Promise {
- if (cacheAvailable) {
- const fileCache = CacheManager.getFileCache();
- await fileCache.delete(cacheKey);
- return;
- }
-
- fallbackCache.delete(cacheKey);
-}
diff --git a/src/services/github/core/content/hydrationStore.ts b/src/services/github/core/content/hydrationStore.ts
index 7e71b2b..2613e7c 100644
--- a/src/services/github/core/content/hydrationStore.ts
+++ b/src/services/github/core/content/hydrationStore.ts
@@ -1,3 +1,5 @@
+// noinspection HttpUrlsUsage
+
import type {
GitHubContent,
InitialContentDirectoryEntry,
diff --git a/src/services/github/core/content/service.ts b/src/services/github/core/content/service.ts
index 8b48207..69b1992 100644
--- a/src/services/github/core/content/service.ts
+++ b/src/services/github/core/content/service.ts
@@ -54,6 +54,7 @@ interface GetContentsOptions {
*
* @param path - 仓库内目录路径,空字符串表示根目录
* @param signal - 可选中断信号,用于取消正在执行的请求
+ * @param options
* @returns 解析后的 GitHub 内容数组
*
* @remarks
@@ -108,11 +109,16 @@ export async function getContents(
apiUrl,
async () => {
logger.debug(`API请求: ${apiUrl}`);
- const result = await fetch(apiUrl, {
+ const requestInit: RequestInit = {
method: 'GET',
- headers: getAuthHeaders(),
- signal: signal ?? null
- });
+ headers: getAuthHeaders()
+ };
+
+ if (signal !== undefined) {
+ requestInit.signal = signal;
+ }
+
+ const result = await fetch(apiUrl, requestInit);
if (!result.ok) {
throw new Error(`HTTP ${result.status.toString()}: ${result.statusText}`);
diff --git a/src/services/github/proxy/ProxyConfig.ts b/src/services/github/proxy/ProxyConfig.ts
index baa01a2..61816c1 100644
--- a/src/services/github/proxy/ProxyConfig.ts
+++ b/src/services/github/proxy/ProxyConfig.ts
@@ -1,5 +1,4 @@
import { getAccessConfig, getProxyConfig } from '@/config';
-import { getForceServerProxy } from '../config';
// 获取配置
export const accessConfig = getAccessConfig();
@@ -8,13 +7,6 @@ const proxyConfig = getProxyConfig();
// 模式设置
export const USE_TOKEN_MODE = accessConfig.useTokenMode;
-/**
- * 检查是否启用强制服务器代理
- *
- * @returns 如果启用返回true
- */
-export const isForceServerProxyEnabled = (): boolean => getForceServerProxy();
-
/**
* 代理服务URL列表
*/
diff --git a/src/services/github/proxy/ProxyService.ts b/src/services/github/proxy/ProxyService.ts
index 4a72177..9f1f546 100644
--- a/src/services/github/proxy/ProxyService.ts
+++ b/src/services/github/proxy/ProxyService.ts
@@ -126,18 +126,14 @@ async function validateProxy(proxyUrl: string, timeout: number): Promise {
method: 'HEAD',
signal: controller.signal
});
-
- window.clearTimeout(timeoutId);
-
if (response.ok) {
const responseTime = Date.now() - startTime;
proxyHealthManager.recordSuccess(proxyUrl, responseTime);
- } else {
- throw new Error(`Proxy validation failed: ${response.status.toString()}`);
+ return;
}
- } catch (error) {
+ throw new Error(`Proxy validation failed: ${response.status.toString()}`);
+ } finally {
window.clearTimeout(timeoutId);
- throw error;
}
}
diff --git a/src/services/github/schemas/apiSchemas.ts b/src/services/github/schemas/apiSchemas.ts
index 983d06d..b55700a 100644
--- a/src/services/github/schemas/apiSchemas.ts
+++ b/src/services/github/schemas/apiSchemas.ts
@@ -1,17 +1,5 @@
import { z } from 'zod';
-// GitHub API基础响应结构
-export const GitHubApiErrorSchema = z.object({
- message: z.string(),
- documentation_url: z.string().optional(),
- errors: z.array(z.object({
- resource: z.string().optional(),
- field: z.string().optional(),
- code: z.string().optional(),
- message: z.string().optional(),
- })).optional(),
-});
-
// GitHub内容项链接结构
export const GitHubLinksSchema = z.object({
self: z.string(),
@@ -74,40 +62,11 @@ export const GitHubSearchResponseSchema = z.object({
items: z.array(GitHubSearchCodeItemSchema),
});
-// 配置信息响应结构
-export const ConfigResponseSchema = z.object({
- status: z.literal('success'),
- data: z.object({
- repoOwner: z.string(),
- repoName: z.string(),
- repoBranch: z.string(),
- }),
-});
-
-// Token状态响应结构
-export const TokenStatusResponseSchema = z.object({
- status: z.literal('success'),
- data: z.object({
- hasTokens: z.boolean(),
- count: z.number(),
- }),
-});
-
-// API通用错误响应结构
-export const ApiErrorResponseSchema = z.object({
- error: z.string(),
- message: z.string().optional(),
-});
-
// 导出所有Schema的类型
-export type GitHubApiError = z.infer;
export type GitHubContentItem = z.infer;
export type GitHubContentsResponse = z.infer;
export type GitHubSearchCodeItem = z.infer;
export type GitHubSearchResponse = z.infer;
-export type ConfigResponse = z.infer;
-export type TokenStatusResponse = z.infer;
-export type ApiErrorResponse = z.infer;
/**
* 验证GitHub内容响应
@@ -131,28 +90,6 @@ export function validateGitHubSearchResponse(data: unknown): GitHubSearchRespons
return GitHubSearchResponseSchema.parse(data);
}
-/**
- * 验证配置响应
- *
- * @param data - 待验证的数据
- * @returns 验证后的ConfigResponse
- * @throws 当数据格式不符合schema时抛出错误
- */
-export function validateConfigResponse(data: unknown): ConfigResponse {
- return ConfigResponseSchema.parse(data);
-}
-
-/**
- * 验证Token状态响应
- *
- * @param data - 待验证的数据
- * @returns 验证后的TokenStatusResponse
- * @throws 当数据格式不符合schema时抛出错误
- */
-export function validateTokenStatusResponse(data: unknown): TokenStatusResponse {
- return TokenStatusResponseSchema.parse(data);
-}
-
/**
* 安全验证GitHub内容响应
*
@@ -203,4 +140,4 @@ export function safeValidateGitHubSearchResponse(data: unknown): {
error: error instanceof Error ? error.message : '未知验证错误'
};
}
-}
\ No newline at end of file
+}
diff --git a/src/theme/animations.ts b/src/theme/animations.ts
index 3d09f21..ab09df8 100644
--- a/src/theme/animations.ts
+++ b/src/theme/animations.ts
@@ -1,56 +1,14 @@
import { keyframes } from '@mui/system';
-/**
- * 旋转动画
- *
- * 用于刷新按钮的旋转效果。
- */
-export const rotateAnimation = keyframes`
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-`;
-/**
- * 脉冲动画
- *
- * 用于主题切换按钮的缩放脉冲效果。
- */
-export const pulseAnimation = keyframes`
- 0% {
- transform: scale(1);
- }
- 50% {
- transform: scale(1.2);
- }
- 100% {
- transform: scale(1);
- }
-`;
-/**
- * 刷新动画
- *
- * 组合旋转和缩放效果,用于刷新按钮。
- */
-export const refreshAnimation = keyframes`
- 0% {
- transform: rotate(0deg) scale(1);
- }
- 50% {
- transform: rotate(180deg) scale(1.1);
- }
- 100% {
- transform: rotate(360deg) scale(1);
- }
-`;
+
+
+
/**
* 淡入动画
- *
+ *
* 元素渐变显示效果。
*/
export const fadeAnimation = keyframes`
@@ -64,7 +22,7 @@ export const fadeAnimation = keyframes`
/**
* 淡出动画
- *
+ *
* 元素渐变消失效果。
*/
export const fadeOutAnimation = keyframes`
@@ -76,37 +34,13 @@ export const fadeOutAnimation = keyframes`
}
`;
-/**
- * 弹跳动画
- *
- * 垂直弹跳效果,用于吸引用户注意。
- */
-export const bounceAnimation = keyframes`
- 0%, 100% {
- transform: translateY(0);
- }
- 50% {
- transform: translateY(-5px);
- }
-`;
-/**
- * 浮动动画
- *
- * 轻微浮动效果,用于FAB按钮。
- */
-export const floatAnimation = keyframes`
- 0%, 100% {
- transform: translateY(0px);
- }
- 50% {
- transform: translateY(-2px);
- }
-`;
+
+
/**
* 缩放进入动画
- *
+ *
* 元素放大淡入效果,用于空状态组件。
*/
export const scaleInAnimation = keyframes`
@@ -118,4 +52,4 @@ export const scaleInAnimation = keyframes`
opacity: 1;
transform: scale(1);
}
-`;
\ No newline at end of file
+`;
diff --git a/src/theme/components/misc.ts b/src/theme/components/misc.ts
index 8700a8e..16067f2 100644
--- a/src/theme/components/misc.ts
+++ b/src/theme/components/misc.ts
@@ -54,4 +54,3 @@ export const miscStyles = {
},
},
};
-
diff --git a/src/theme/g3Curves.ts b/src/theme/g3Curves.ts
index a8b81a8..a845723 100644
--- a/src/theme/g3Curves.ts
+++ b/src/theme/g3Curves.ts
@@ -24,9 +24,9 @@ const DEFAULT_G3_CONFIG: Required = {
/**
* 创建G3曲线边框半径
- *
+ *
* 根据配置生成带有G3曲线效果的CSS border-radius值。
- *
+ *
* @param config - G3曲线配置
* @returns CSS边框半径字符串
*/
@@ -36,25 +36,6 @@ export function createG3BorderRadius(config: G3CurveConfig): string {
return `${Math.round(adjustedRadius).toString()}px`;
}
-/**
- * 创建G3曲线样式对象
- *
- * 生成包含G3曲线效果的完整CSS样式对象。
- *
- * @param config - G3曲线配置
- * @returns React CSS属性对象
- */
-export function createG3Style(config: G3CurveConfig): React.CSSProperties {
- const { radius, smoothness = DEFAULT_G3_CONFIG.smoothness } = config;
- const g3Radius = createG3BorderRadius(config);
- return {
- borderRadius: g3Radius,
- '--g3-radius': `${radius.toString()}px`,
- '--g3-smoothness': smoothness.toString(),
- transition: 'border-radius 0.2s ease-out, box-shadow 0.2s ease-out',
- } as React.CSSProperties;
-}
-
// 预定义的曲线配置
export const G3_PRESETS = {
// 文件列表项
@@ -118,44 +99,6 @@ export const G3_PRESETS = {
} as G3CurveConfig,
} as const;
-/**
- * G3样式工具集
- *
- * 提供预设配置的快捷访问方法。
- */
-export const g3Styles = {
- fileListItem: () => createG3Style(G3_PRESETS.fileListItem),
- fileListContainer: () => createG3Style(G3_PRESETS.fileListContainer),
- card: () => createG3Style(G3_PRESETS.card),
- button: () => createG3Style(G3_PRESETS.button),
- dialog: () => createG3Style(G3_PRESETS.dialog),
- image: () => createG3Style(G3_PRESETS.image),
- breadcrumb: () => createG3Style(G3_PRESETS.breadcrumb),
- breadcrumbItem: () => createG3Style(G3_PRESETS.breadcrumbItem),
- tooltip: () => createG3Style(G3_PRESETS.tooltip),
- skeletonLine: () => createG3Style(G3_PRESETS.skeletonLine),
-};
-
-/**
- * 创建响应式G3样式
- *
- * 为桌面端和移动端分别生成G3曲线样式。
- *
- * @param desktopConfig - 桌面端配置
- * @param mobileConfig - 移动端配置(可选,默认使用桌面端配置)
- * @returns 包含桌面端和移动端样式的对象
- */
-export function createResponsiveG3Style(
- desktopConfig: G3CurveConfig,
- mobileConfig?: Partial
-): { desktop: React.CSSProperties; mobile: React.CSSProperties } {
- const mobile = { ...desktopConfig, ...mobileConfig };
-
- return {
- desktop: createG3Style(desktopConfig),
- mobile: createG3Style(mobile),
- };
-}
/**
* 响应式圆角配置接口
@@ -190,9 +133,9 @@ export const RESPONSIVE_G3_PRESETS = {
/**
* 获取响应式圆角样式
- *
+ *
* 根据屏幕大小返回对应的边框半径。
- *
+ *
* @param preset - 响应式G3配置
* @param isSmallScreen - 是否为小屏幕
* @returns CSS边框半径字符串
@@ -207,38 +150,26 @@ export function getResponsiveG3BorderRadius(
/**
* 响应式G3样式工具集
- *
+ *
* 提供响应式预设配置的快捷访问方法。
*/
export const responsiveG3Styles = {
- fileListContainer: (isSmallScreen: boolean) =>
+ fileListContainer: (isSmallScreen: boolean) =>
getResponsiveG3BorderRadius(RESPONSIVE_G3_PRESETS.fileListContainer, isSmallScreen),
- readmeContainer: (isSmallScreen: boolean) =>
+ readmeContainer: (isSmallScreen: boolean) =>
getResponsiveG3BorderRadius(RESPONSIVE_G3_PRESETS.readmeContainer, isSmallScreen),
- card: (isSmallScreen: boolean) =>
+ card: (isSmallScreen: boolean) =>
getResponsiveG3BorderRadius(RESPONSIVE_G3_PRESETS.card, isSmallScreen),
};
/**
* G3边框半径快捷函数
- *
+ *
* 用于Emotion styled-components的简化API。
- *
+ *
* @param config - G3曲线配置
* @returns CSS边框半径字符串
*/
export function g3BorderRadius(config: G3CurveConfig): string {
return createG3BorderRadius(config);
}
-
-/**
- * G3样式快捷函数
- *
- * 用于Material-UI sx prop的简化API。
- *
- * @param config - G3曲线配置
- * @returns React CSS属性对象
- */
-export function g3Sx(config: G3CurveConfig): React.CSSProperties {
- return createG3Style(config);
-}
diff --git a/src/types/errors.ts b/src/types/errors.ts
index 0984353..47faeea 100644
--- a/src/types/errors.ts
+++ b/src/types/errors.ts
@@ -133,15 +133,6 @@ export type AppError =
| SystemError;
// 类型守卫函数
-export function isAPIError(error: AppError): error is APIError {
- return (
- error.category === ErrorCategory.API &&
- 'statusCode' in error &&
- 'endpoint' in error &&
- 'method' in error
- );
-}
-
export function isNetworkError(error: AppError): error is NetworkError {
return error.category === ErrorCategory.NETWORK && 'url' in error;
}
@@ -149,8 +140,8 @@ export function isNetworkError(error: AppError): error is NetworkError {
export function isGitHubError(error: AppError): error is GitHubError {
return (
error.category === ErrorCategory.API &&
- ('rateLimitRemaining' in error ||
- 'rateLimitReset' in error ||
+ ('rateLimitRemaining' in error ||
+ 'rateLimitReset' in error ||
'documentationUrl' in error)
);
}
@@ -163,26 +154,6 @@ export function isFileOperationError(error: AppError): error is FileOperationErr
);
}
-export function isComponentError(error: AppError): error is ComponentError {
- return error.category === ErrorCategory.COMPONENT && 'componentName' in error;
-}
-
-export function isAuthError(error: AppError): error is AuthError {
- return error.category === ErrorCategory.AUTH;
-}
-
-export function isValidationError(error: AppError): error is ValidationError {
- return (
- error.category === ErrorCategory.VALIDATION &&
- 'field' in error &&
- 'value' in error
- );
-}
-
-export function isSystemError(error: AppError): error is SystemError {
- return error.category === ErrorCategory.SYSTEM;
-}
-
// 错误上下文接口
export interface ErrorContext {
userId?: string;
@@ -203,10 +174,3 @@ export interface ErrorHandlerConfig {
retryAttempts: number;
retryDelay: number;
}
-
-// 错误恢复策略
-export interface ErrorRecoveryStrategy {
- canRecover: (error: AppError) => boolean;
- recover: (error: AppError) => Promise | void;
- fallback?: () => React.ReactNode;
-}
diff --git a/src/types/index.ts b/src/types/index.ts
index 7a5cce9..e57d13f 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -4,7 +4,6 @@
* 将所有类型定义按功能域拆分到不同文件中,并在此统一导出。
*/
-import type { OptionsObject as NotistackOptionsObject } from 'notistack';
// 导出错误相关类型
export * from './errors';
@@ -15,8 +14,6 @@ export type * from './preview';
// 导出下载相关类型
export type * from './download';
-// 导出状态相关类型
-export type * from './state';
/**
* GitHub仓库内容项接口
@@ -48,22 +45,6 @@ export interface BreadcrumbSegment {
path: string;
}
-/**
- * 通知组件扩展选项接口
- */
-export interface OptionsObject extends NotistackOptionsObject {
- /** 是否隐藏关闭按钮 */
- hideCloseButton?: boolean;
-}
-
-/**
- * 进度通知选项接口
- */
-export interface ProgressSnackbarOptions extends OptionsObject {
- /** 进度百分比 */
- progress?: number;
-}
-
/**
* 首屏注水的目录条目
*/
diff --git a/src/types/state.ts b/src/types/state.ts
deleted file mode 100644
index a64576f..0000000
--- a/src/types/state.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * 应用状态相关类型定义
- *
- * 包含应用级别的状态类型定义。
- */
-
-import type { GitHubContent } from './index';
-import type { PreviewState } from './preview';
-import type { DownloadState } from './download';
-
-/**
- * 应用状态接口
- */
-export interface AppState {
- preview: PreviewState;
- download: DownloadState;
- currentPath: string;
- contents: GitHubContent[];
- readmeContent: string | null;
- loading: boolean;
- loadingReadme: boolean;
- error: string | null;
- refreshTrigger: number;
-}
-
diff --git a/src/utils/cache/SmartCache.ts b/src/utils/cache/SmartCache.ts
index bc601a3..bd472c0 100644
--- a/src/utils/cache/SmartCache.ts
+++ b/src/utils/cache/SmartCache.ts
@@ -205,75 +205,4 @@ export class SmartCache {
}
}
}
-
- /**
- * 获取缓存统计信息
- * @returns 缓存统计对象
- */
- getStats(): {
- size: number;
- maxSize: number;
- hitRate: number;
- totalHits: number;
- } {
- let totalHits = 0;
-
- for (const entry of this.cache.values()) {
- totalHits += entry.hitCount;
- }
-
- return {
- size: this.cache.size,
- maxSize: this.maxSize,
- hitRate: this.cache.size > 0 ? totalHits / this.cache.size : 0,
- totalHits
- };
- }
}
-
-/**
- * 创建一个弱引用缓存
- *
- * 使用 WeakMap 实现,适合缓存对象类型的键。
- * 当键对象被垃圾回收时,缓存条目也会自动清理。
- */
-export class WeakCache {
- private cache = new WeakMap();
-
- /**
- * 获取缓存值
- * @param key - 缓存键(必须是对象)
- * @returns 缓存的值,如果不存在则返回 undefined
- */
- get(key: K): V | undefined {
- return this.cache.get(key);
- }
-
- /**
- * 设置缓存值
- * @param key - 缓存键(必须是对象)
- * @param value - 要缓存的值
- */
- set(key: K, value: V): void {
- this.cache.set(key, value);
- }
-
- /**
- * 检查是否存在指定键
- * @param key - 缓存键
- * @returns 是否存在
- */
- has(key: K): boolean {
- return this.cache.has(key);
- }
-
- /**
- * 删除指定键的缓存
- * @param key - 缓存键
- * @returns 是否删除成功
- */
- delete(key: K): boolean {
- return this.cache.delete(key);
- }
-}
-
diff --git a/src/utils/content/prismHighlighter.ts b/src/utils/content/prismHighlighter.ts
index 7805fe2..f9c979b 100644
--- a/src/utils/content/prismHighlighter.ts
+++ b/src/utils/content/prismHighlighter.ts
@@ -48,31 +48,6 @@ import 'prismjs/components/prism-dart';
import 'prismjs/components/prism-git';
import 'prismjs/components/prism-batch';
-/**
- * 使用 Prism.js 高亮代码内容
- *
- * @param code - 要高亮的代码内容
- * @param language - Prism 语言标识符,如果为 null 则只转义 HTML
- * @returns 高亮后的 HTML 字符串
- */
-export function highlightCode(code: string, language: string | null): string {
- if (language === null || language === '' || Prism.languages[language] === undefined) {
- // 如果没有支持的语言,使用 Prism 的转义函数避免 XSS
- const encoded = Prism.util.encode(code);
- return typeof encoded === 'string' ? encoded : code.replace(/&/g, '&').replace(//g, '>');
- }
-
- try {
- const grammar = Prism.languages[language];
- return Prism.highlight(code, grammar, language);
- } catch (error) {
- // 如果高亮失败,只转义 HTML
- logger.warn('Prism highlight failed:', error);
- const encoded = Prism.util.encode(code);
- return typeof encoded === 'string' ? encoded : code.replace(/&/g, '&').replace(//g, '>');
- }
-}
-
/**
* 高亮文本文件的每一行
*
diff --git a/src/utils/crypto/hashUtils.ts b/src/utils/crypto/hashUtils.ts
index 8b20b1d..f2128f7 100644
--- a/src/utils/crypto/hashUtils.ts
+++ b/src/utils/crypto/hashUtils.ts
@@ -5,45 +5,6 @@
* 用于生成可靠的缓存键、数据指纹等。
*/
-/**
- * 哈希算法类型
- */
-export type HashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
-
-/**
- * 使用 Web Crypto API 生成字符串的哈希值
- *
- * @param input - 要哈希的字符串
- * @param algorithm - 哈希算法,默认为 SHA-256
- * @param length - 返回的哈希长度(字符数),默认为16
- * @returns Promise,解析为十六进制格式的哈希字符串
- *
- * @example
- * const hash = await hashString('my-data', 'SHA-256', 16);
- * // 返回: 'a3f2d9e8b1c4f7a0'
- */
-export async function hashString(
- input: string,
- algorithm: HashAlgorithm = 'SHA-256',
- length = 16
-): Promise {
- // 将字符串编码为 Uint8Array
- const encoder = new TextEncoder();
- const data = encoder.encode(input);
-
- // 使用 Web Crypto API 计算哈希
- const hashBuffer = await crypto.subtle.digest(algorithm, data);
-
- // 转换为十六进制字符串
- const hashArray = Array.from(new Uint8Array(hashBuffer));
- const hashHex = hashArray
- .map(byte => byte.toString(16).padStart(2, '0'))
- .join('');
-
- // 返回指定长度的哈希
- return hashHex.substring(0, length);
-}
-
/**
* 同步哈希函数(使用快速非加密哈希)
*
@@ -97,83 +58,3 @@ export function hashStringSync(str: string, seed = 0): string {
* const secureKey = await generateCacheKey(['sensitive', 'data'], 'sk_', true);
* // 返回: 'sk_a3f2d9e8b1c4f7a0'
*/
-export async function generateCacheKey(
- components: string[],
- prefix = '',
- useSecure = false
-): Promise {
- const keyString = components.join(':');
-
- if (useSecure) {
- // 使用加密安全的哈希
- const hash = await hashString(keyString, 'SHA-256', 16);
- return `${prefix}${hash}`;
- } else {
- // 使用快速同步哈希
- const hash = hashStringSync(keyString);
- return `${prefix}${hash}`;
- }
-}
-
-/**
- * 生成数据指纹
- *
- * 为数据对象生成一个唯一的指纹,用于版本控制或变更检测。
- *
- * @param data - 要生成指纹的数据对象
- * @param algorithm - 哈希算法,默认为 SHA-256
- * @returns Promise,解析为指纹字符串
- *
- * @example
- * const fingerprint = await generateDataFingerprint({
- * files: [...],
- * timestamp: Date.now()
- * });
- */
-export async function generateDataFingerprint(
- data: unknown,
- algorithm: HashAlgorithm = 'SHA-256'
-): Promise {
- // 将数据序列化为规范化的 JSON 字符串
- const jsonString = JSON.stringify(data, Object.keys(data as object).sort());
- return hashString(jsonString, algorithm, 32);
-}
-
-/**
- * 批量生成缓存键
- *
- * 为多个项目批量生成缓存键,提高性能。
- *
- * @param items - 项目数组,每项包含缓存键的组成部分
- * @param prefix - 可选的前缀
- * @returns 缓存键数组
- *
- * @example
- * const keys = batchGenerateCacheKeys([
- * ['branch1', 'path1'],
- * ['branch1', 'path2'],
- * ['branch2', 'path1']
- * ], 'c_');
- */
-export function batchGenerateCacheKeys(
- items: string[][],
- prefix = ''
-): string[] {
- // 使用同步哈希以提高批量处理性能
- return items.map(components => {
- const keyString = components.join(':');
- const hash = hashStringSync(keyString);
- return `${prefix}${hash}`;
- });
-}
-
-/**
- * 哈希工具命名空间
- */
-export const HashUtils = {
- hashString,
- hashStringSync,
- generateCacheKey,
- generateDataFingerprint,
- batchGenerateCacheKeys
-} as const;
diff --git a/src/utils/data-structures/MinHeap.ts b/src/utils/data-structures/MinHeap.ts
index d9e8f76..bae61c6 100644
--- a/src/utils/data-structures/MinHeap.ts
+++ b/src/utils/data-structures/MinHeap.ts
@@ -70,15 +70,6 @@ export class MinHeap {
return min;
}
- /**
- * 查看最小元素(不删除)
- *
- * @returns 最小元素,如果堆为空则返回 undefined
- */
- peek(): T | undefined {
- return this.heap[0];
- }
-
/**
* 提取最小的 k 个元素
*
@@ -106,13 +97,6 @@ export class MinHeap {
return this.heap.length;
}
- /**
- * 检查堆是否为空
- */
- isEmpty(): boolean {
- return this.heap.length === 0;
- }
-
/**
* 清空堆
*/
@@ -184,14 +168,6 @@ export class MinHeap {
}
}
- /**
- * 获取堆的数组表示(用于调试)
- *
- * @returns 堆的数组副本
- */
- toArray(): T[] {
- return [...this.heap];
- }
}
/**
diff --git a/src/utils/data-structures/TimeWheel.ts b/src/utils/data-structures/TimeWheel.ts
index bcbc246..1abe7dc 100644
--- a/src/utils/data-structures/TimeWheel.ts
+++ b/src/utils/data-structures/TimeWheel.ts
@@ -64,17 +64,6 @@ export class TimeWheel {
logger.debug(`时间轮已启动,槽大小: ${this.slotDuration.toString()}ms, 总槽数: ${this.totalSlots.toString()}, 滴答间隔: ${this.tickInterval.toString()}ms`);
}
- /**
- * 停止时间轮
- */
- stop(): void {
- if (this.tickTimer !== null) {
- clearInterval(this.tickTimer);
- this.tickTimer = null;
- logger.debug('时间轮已停止');
- }
- }
-
/**
* 添加条目
*
@@ -281,15 +270,6 @@ export class TimeWheel {
};
}
- /**
- * 销毁时间轮
- *
- * 停止时钟并清空所有数据。
- */
- destroy(): void {
- this.stop();
- this.clear();
- }
}
/**
@@ -307,4 +287,3 @@ export function createTimeWheel(options?: {
wheel.start();
return wheel;
}
-
diff --git a/src/utils/error/core/ErrorFactory.ts b/src/utils/error/core/ErrorFactory.ts
index 3e137d4..105c900 100644
--- a/src/utils/error/core/ErrorFactory.ts
+++ b/src/utils/error/core/ErrorFactory.ts
@@ -1,10 +1,8 @@
import type {
BaseError,
APIError,
- NetworkError,
GitHubError,
ComponentError,
- FileOperationError,
ErrorContext
} from '@/types/errors';
import { ErrorLevel, ErrorCategory } from '@/types/errors';
@@ -15,19 +13,12 @@ import { ErrorLevel, ErrorCategory } from '@/types/errors';
* 负责创建各种类型的结构化错误对象。
*/
export class ErrorFactory {
- private sessionId: string;
+ private readonly sessionId: string;
constructor(sessionId: string) {
this.sessionId = sessionId;
}
- /**
- * 更新会话ID
- */
- public updateSessionId(newSessionId: string): void {
- this.sessionId = newSessionId;
- }
-
/**
* 获取基础错误上下文
*/
@@ -130,40 +121,6 @@ export class ErrorFactory {
};
}
- /**
- * 创建网络错误
- *
- * @param message - 错误消息
- * @param url - 请求URL
- * @param timeout - 是否为超时错误
- * @param retryCount - 重试次数
- * @param context - 额外的上下文信息
- * @returns 网络错误对象
- */
- public createNetworkError(
- message: string,
- url: string,
- timeout = false,
- retryCount = 0,
- context?: Record
- ): NetworkError {
- const baseError = this.createBaseError(
- timeout ? 'NETWORK_TIMEOUT' : 'NETWORK_ERROR',
- message,
- ErrorLevel.ERROR,
- ErrorCategory.NETWORK,
- context
- );
-
- return {
- ...baseError,
- category: ErrorCategory.NETWORK,
- url,
- timeout,
- retryCount
- };
- }
-
/**
* 创建组件错误
*
@@ -195,91 +152,6 @@ export class ErrorFactory {
};
}
- /**
- * 创建文件操作错误
- *
- * @param fileName - 文件名
- * @param operation - 操作类型
- * @param message - 错误消息
- * @param fileSize - 文件大小(可选)
- * @param context - 额外的上下文信息
- * @returns 文件操作错误对象
- */
- public createFileOperationError(
- fileName: string,
- operation: 'read' | 'write' | 'download' | 'compress' | 'parse',
- message: string,
- fileSize?: number,
- context?: Record
- ): FileOperationError {
- const baseError = this.createBaseError(
- `FILE_${operation.toUpperCase()}_ERROR`,
- message,
- ErrorLevel.ERROR,
- ErrorCategory.FILE_OPERATION,
- context
- );
-
- return {
- ...baseError,
- category: ErrorCategory.FILE_OPERATION,
- fileName,
- ...(fileSize !== undefined ? { fileSize } : {}),
- operation
- };
- }
-
- /**
- * 处理API错误响应
- *
- * 从axios或fetch错误中提取信息并创建结构化的API错误。
- *
- * @param error - 错误对象
- * @param endpoint - API端点
- * @param method - HTTP方法
- * @returns API错误或GitHub错误对象
- */
- public handleAPIError(error: unknown, endpoint: string, method: string): APIError | GitHubError {
- const errorObj = error as {
- response?: {
- status?: number;
- data?: { message?: string };
- headers?: Record;
- };
- message?: string;
- config?: { data?: unknown };
- };
-
- const statusCode = errorObj.response?.status ?? 0;
- const message = errorObj.response?.data?.message ?? errorObj.message ?? '网络请求失败';
-
- // GitHub API特定处理
- if (endpoint.includes('api.github.com') || endpoint.includes('github')) {
- const rateLimitRemaining = errorObj.response?.headers?.['x-ratelimit-remaining'];
- const rateLimitReset = errorObj.response?.headers?.['x-ratelimit-reset'];
-
- return this.createGitHubError(
- message,
- statusCode,
- endpoint,
- method,
- rateLimitRemaining !== undefined ? {
- remaining: parseInt(rateLimitRemaining, 10),
- reset: parseInt(rateLimitReset ?? '0', 10)
- } : undefined,
- {
- requestData: errorObj.config?.data,
- responseData: errorObj.response?.data
- }
- );
- }
-
- return this.createAPIError(message, statusCode, endpoint, method, {
- requestData: errorObj.config?.data,
- responseData: errorObj.response?.data
- });
- }
-
/**
* 根据状态码确定错误级别
*/
@@ -296,4 +168,3 @@ export class ErrorFactory {
return ErrorLevel.INFO;
}
}
-
diff --git a/src/utils/error/core/ErrorHistory.ts b/src/utils/error/core/ErrorHistory.ts
index ca54bdf..728c852 100644
--- a/src/utils/error/core/ErrorHistory.ts
+++ b/src/utils/error/core/ErrorHistory.ts
@@ -1,5 +1,4 @@
import type { AppError } from '@/types/errors';
-import { ErrorCategory } from '@/types/errors';
/**
* 错误历史管理类
@@ -36,64 +35,6 @@ export class ErrorHistory {
}
}
- /**
- * 获取错误历史
- *
- * @param category - 可选的错误分类过滤
- * @param limit - 返回的最大错误数量,默认20
- * @returns 错误历史数组
- */
- public getErrorHistory(category?: ErrorCategory, limit = 20): AppError[] {
- let history = this.errorHistory;
-
- if (category !== undefined) {
- history = history.filter(error => error.category === category);
- }
-
- return history.slice(0, limit);
- }
-
- /**
- * 清理错误历史
- *
- * 清空所有记录的错误历史。
- */
- public clearErrorHistory(): void {
- this.errorHistory = [];
- }
-
- /**
- * 获取错误统计
- *
- * 返回各类错误的数量统计。
- *
- * @returns 错误分类统计对象
- */
- public getErrorStats(): Record {
- const stats: Record = {
- [ErrorCategory.NETWORK]: 0,
- [ErrorCategory.API]: 0,
- [ErrorCategory.AUTH]: 0,
- [ErrorCategory.VALIDATION]: 0,
- [ErrorCategory.FILE_OPERATION]: 0,
- [ErrorCategory.COMPONENT]: 0,
- [ErrorCategory.SYSTEM]: 0
- };
-
- this.errorHistory.forEach(error => {
- stats[error.category] = stats[error.category] + 1;
- });
-
- return stats;
- }
-
- /**
- * 获取历史记录大小
- */
- public getHistorySize(): number {
- return this.errorHistory.length;
- }
-
/**
* 清理超时的旧错误
*/
@@ -104,4 +45,3 @@ export class ErrorHistory {
);
}
}
-
diff --git a/src/utils/error/core/ErrorLogger.ts b/src/utils/error/core/ErrorLogger.ts
index 7a5d6af..859acc9 100644
--- a/src/utils/error/core/ErrorLogger.ts
+++ b/src/utils/error/core/ErrorLogger.ts
@@ -8,20 +8,13 @@ import { createScopedLogger } from '../../logging/logger';
* 负责将错误记录到控制台或其他日志系统。
*/
export class ErrorLogger {
- private enableLogging: boolean;
+ private readonly enableLogging: boolean;
private readonly scopedLogger = createScopedLogger('ErrorManager');
constructor(enableLogging = true) {
this.enableLogging = enableLogging;
}
- /**
- * 更新日志配置
- */
- public setLoggingEnabled(enabled: boolean): void {
- this.enableLogging = enabled;
- }
-
/**
* 记录错误日志
*/
@@ -58,4 +51,3 @@ export class ErrorLogger {
return `[${error.category}] ${error.code}: ${error.message}`;
}
}
-
diff --git a/src/utils/error/core/ErrorManager.ts b/src/utils/error/core/ErrorManager.ts
index a242031..ece4867 100644
--- a/src/utils/error/core/ErrorManager.ts
+++ b/src/utils/error/core/ErrorManager.ts
@@ -2,11 +2,8 @@ import type {
AppError,
ErrorContext,
ErrorHandlerConfig,
- APIError,
- NetworkError,
GitHubError,
- ComponentError,
- FileOperationError
+ ComponentError
} from '@/types/errors';
import { ErrorLevel, ErrorCategory } from '@/types/errors';
import { createScopedLogger } from '../../logging/logger';
@@ -94,19 +91,6 @@ class ErrorManagerClass {
return appError;
}
- /**
- * 创建API错误
- */
- public createAPIError(
- message: string,
- statusCode: number,
- endpoint: string,
- method: string,
- context?: Record
- ): APIError {
- return this.factory.createAPIError(message, statusCode, endpoint, method, context);
- }
-
/**
* 创建GitHub特定错误
*/
@@ -121,19 +105,6 @@ class ErrorManagerClass {
return this.factory.createGitHubError(message, statusCode, endpoint, method, rateLimitInfo, context);
}
- /**
- * 创建网络错误
- */
- public createNetworkError(
- message: string,
- url: string,
- timeout = false,
- retryCount = 0,
- context?: Record
- ): NetworkError {
- return this.factory.createNetworkError(message, url, timeout, retryCount, context);
- }
-
/**
* 创建组件错误
*/
@@ -146,26 +117,6 @@ class ErrorManagerClass {
return this.factory.createComponentError(componentName, message, props, context);
}
- /**
- * 创建文件操作错误
- */
- public createFileOperationError(
- fileName: string,
- operation: 'read' | 'write' | 'download' | 'compress' | 'parse',
- message: string,
- fileSize?: number,
- context?: Record
- ): FileOperationError {
- return this.factory.createFileOperationError(fileName, operation, message, fileSize, context);
- }
-
- /**
- * 处理API错误响应
- */
- public handleAPIError(error: unknown, endpoint: string, method: string): APIError | GitHubError {
- return this.factory.handleAPIError(error, endpoint, method);
- }
-
// 检查是否为AppError
private isAppError(error: unknown): error is AppError {
return error !== null && typeof error === 'object' &&
@@ -195,47 +146,6 @@ class ErrorManagerClass {
}
}
- /**
- * 获取错误历史
- */
- public getErrorHistory(category?: ErrorCategory, limit = 20): AppError[] {
- return this.history.getErrorHistory(category, limit);
- }
-
- /**
- * 清理错误历史
- */
- public clearErrorHistory(): void {
- this.history.clearErrorHistory();
- }
-
- /**
- * 获取错误统计
- */
- public getErrorStats(): Record {
- return this.history.getErrorStats();
- }
-
- /**
- * 更新错误处理配置
- */
- public updateConfig(newConfig: Partial): void {
- this.config = { ...this.config, ...newConfig };
-
- // 更新子模块配置
- if (newConfig.enableConsoleLogging !== undefined) {
- this.errorLogger.setLoggingEnabled(newConfig.enableConsoleLogging);
- }
- }
-
- /**
- * 重置错误会话
- */
- public resetSession(): void {
- this.sessionId = this.generateSessionId();
- this.factory.updateSessionId(this.sessionId);
- this.history.clearErrorHistory();
- }
}
/**
diff --git a/src/utils/error/errorHandler.ts b/src/utils/error/errorHandler.ts
index 97e5e7a..838e397 100644
--- a/src/utils/error/errorHandler.ts
+++ b/src/utils/error/errorHandler.ts
@@ -130,45 +130,3 @@ export function handleError(
reportError(appError, context);
}
}
-
-/**
- * 处理网络错误
- */
-export function handleNetworkError(
- error: unknown,
- context: string,
- options: ErrorHandlerOptions = {}
-): void {
- handleError(error, context, {
- ...options,
- userMessage: options.userMessage ?? '网络请求失败,请检查网络连接'
- });
-}
-
-/**
- * 处理 API 错误
- */
-export function handleApiError(
- error: unknown,
- context: string,
- options: ErrorHandlerOptions = {}
-): void {
- handleError(error, context, {
- ...options,
- userMessage: options.userMessage ?? 'API 请求失败,请稍后重试'
- });
-}
-
-/**
- * 处理文件操作错误
- */
-export function handleFileError(
- error: unknown,
- context: string,
- options: ErrorHandlerOptions = {}
-): void {
- handleError(error, context, {
- ...options,
- userMessage: options.userMessage ?? '文件操作失败,请重试'
- });
-}
diff --git a/src/utils/events/eventEmitter.ts b/src/utils/events/eventEmitter.ts
index c2dbab8..05e0c7e 100644
--- a/src/utils/events/eventEmitter.ts
+++ b/src/utils/events/eventEmitter.ts
@@ -28,43 +28,6 @@ export interface AppEvents {
export class TypedEventEmitter> {
private events = new Map void>>();
- /**
- * 订阅事件
- *
- * @param event - 事件名称(类型安全)
- * @param callback - 事件回调函数(类型安全)
- * @returns 取消订阅的函数
- *
- * @example
- * ```typescript
- * const unsubscribe = eventEmitter.subscribe('refresh_content', (data) => {
- * console.log('路径:', data.path); // ✅ 类型安全
- * });
- * ```
- */
- subscribe(
- event: K,
- callback: (data: T[K]) => void
- ): () => void {
- if (!this.events.has(event)) {
- this.events.set(event, new Set());
- }
-
- const handlers = this.events.get(event);
- if (handlers !== undefined) {
- handlers.add(callback as (data: T[keyof T]) => void);
- logger.debug(`事件订阅: ${String(event)}, 当前订阅者数量: ${handlers.size.toString()}`);
- }
-
- return () => {
- const handlers = this.events.get(event);
- if (handlers !== undefined) {
- handlers.delete(callback as (data: T[keyof T]) => void);
- logger.debug(`取消事件订阅: ${String(event)}, 剩余订阅者数量: ${handlers.size.toString()}`);
- }
- };
- }
-
/**
* 分发事件
*
@@ -94,50 +57,12 @@ export class TypedEventEmitter> {
logger.debug(`事件分发: ${String(event)}`);
}
- /**
- * 取消订阅(别名方法)
- */
- on(event: K, callback: (data: T[K]) => void): () => void {
- return this.subscribe(event, callback);
- }
-
- /**
- * 触发事件(别名方法)
- */
- emit(event: K, data: T[K]): void {
- this.dispatch(event, data);
- }
-
- /**
- * 移除特定的监听器
- */
- removeListener(event: K, callback: (data: T[K]) => void): void {
- const handlers = this.events.get(event);
- if (handlers !== undefined) {
- handlers.delete(callback as (data: T[keyof T]) => void);
- }
- }
-
- /**
- * 移除事件的所有监听器
- */
- removeAllListeners(event: keyof T): void {
- this.events.delete(event);
- }
-
/**
* 清空所有事件监听器
*/
clear(): void {
this.events.clear();
}
-
- /**
- * 获取事件监听器数量
- */
- listenerCount(event: keyof T): number {
- return this.events.get(event)?.size ?? 0;
- }
}
/**
diff --git a/src/utils/files/fileHelpers.ts b/src/utils/files/fileHelpers.ts
index 6d7d760..bac082c 100644
--- a/src/utils/files/fileHelpers.ts
+++ b/src/utils/files/fileHelpers.ts
@@ -1,5 +1,4 @@
import {
- Description as FileIcon,
PictureAsPdf as PdfIcon,
Article as MarkdownIcon,
TextSnippet as TxtIcon,
@@ -12,9 +11,10 @@ import {
Code as CodeIcon,
Archive as ArchiveIcon
} from '@mui/icons-material';
+import type { ElementType } from "react";
// 文件扩展名与图标映射
-export const fileExtensionIcons: Record = {
+export const fileExtensionIcons: Record = {
zip: ArchiveIcon, rar: ArchiveIcon, '7z': ArchiveIcon, tar: ArchiveIcon, gz: ArchiveIcon,
pdf: PdfIcon,
doc: DocIcon, docx: DocIcon,
@@ -37,25 +37,6 @@ export const fileExtensionIcons: Record = {
sql: CodeIcon, cs: CodeIcon, fs: CodeIcon, fsx: CodeIcon, vb: CodeIcon,
};
-/**
- * 获取文件图标
- *
- * 根据文件扩展名返回对应的Material-UI图标组件。
- *
- * @param filename - 文件名
- * @returns 图标组件
- */
-export const getFileIcon = (filename: string): React.ElementType => {
- const extension = filename.split('.').pop()?.toLowerCase();
- if (typeof extension === 'string' && extension.length > 0) {
- const icon = fileExtensionIcons[extension];
- if (typeof icon !== 'undefined') {
- return icon;
- }
- }
- return FileIcon;
-};
-
/**
* 通用的文件扩展名检测函数
*
@@ -209,6 +190,4 @@ export const isTextFile = (filename: string): boolean => {
}
return lowerCaseName.startsWith('.') && TEXT_FILE_NAMES.has(lowerCaseName);
-
-
};
diff --git a/src/utils/format/formatters.ts b/src/utils/format/formatters.ts
index ccd4226..b3b70c3 100644
--- a/src/utils/format/formatters.ts
+++ b/src/utils/format/formatters.ts
@@ -1,26 +1,3 @@
-/**
- * 格式化日期字符串
- *
- * 将ISO日期字符串转换为本地化的日期时间格式。
- *
- * @param dateString - ISO格式的日期字符串
- * @returns 格式化后的日期字符串
- */
-export const formatDate = (dateString: string): string => {
- try {
- const date = new Date(dateString);
- return new Intl.DateTimeFormat('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- }).format(date);
- } catch (_e) {
- return dateString;
- }
-};
-
/**
* 格式化文件大小
*
@@ -43,4 +20,4 @@ export const formatFileSize = (bytes: number): string => {
const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(2));
return `${String(formattedSize)} ${sizeUnit}`;
-};
\ No newline at end of file
+};
diff --git a/src/utils/i18n/i18n.ts b/src/utils/i18n/i18n.ts
index 39b895b..1643e1c 100644
--- a/src/utils/i18n/i18n.ts
+++ b/src/utils/i18n/i18n.ts
@@ -20,9 +20,7 @@ const formatOptions = (
* 提供国际化翻译功能
*/
export class I18N {
- private readonly locale: Locale;
private readonly translator: ITranslator;
- private readonly keys: ILocaleJSON;
private readonly alwaysShowScreamers: boolean;
/**
@@ -39,7 +37,6 @@ export class I18N {
alwaysShowScreamers = false,
isLoading = false,
) {
- this.locale = locale;
// 使用闭包捕获 isLoading 状态
const shouldWarn = !isLoading;
this.translator = new Translator(locale, translation, {
@@ -57,35 +54,9 @@ export class I18N {
}
},
});
- this.keys = translation;
this.alwaysShowScreamers = alwaysShowScreamers;
}
- /**
- * 获取当前语言代码
- */
- get currentLocale(): Locale {
- return this.locale;
- }
-
- /**
- * 获取当前翻译键
- */
- get currentKeys(): ILocaleJSON {
- return this.keys;
- }
-
- /**
- * 获取未插值的字符串
- */
- getUninterpolatedString(key: string): string {
- if (this.alwaysShowScreamers) {
- return key;
- } else {
- return this.translator.getUninterpolatedString(key);
- }
- }
-
/**
* 翻译方法
*
@@ -105,4 +76,3 @@ export class I18N {
}
export default I18N;
-
diff --git a/src/utils/i18n/translator.ts b/src/utils/i18n/translator.ts
index 668ff7e..6b26eb5 100644
--- a/src/utils/i18n/translator.ts
+++ b/src/utils/i18n/translator.ts
@@ -174,14 +174,6 @@ class Translator implements ITranslator {
return getNestedValue(this.translations, key);
}
- /**
- * 获取未插值的字符串
- */
- getUninterpolatedString(key: string): string {
- const keyValue = this.getValue(key);
- return keyValue ?? this.onMissingKeyFn(key);
- }
-
/**
* 翻译字符串
* 支持插值和复数形式
@@ -224,4 +216,3 @@ class Translator implements ITranslator {
}
export default Translator;
-
diff --git a/src/utils/i18n/types.ts b/src/utils/i18n/types.ts
index 5dfed3e..a92bdaf 100644
--- a/src/utils/i18n/types.ts
+++ b/src/utils/i18n/types.ts
@@ -34,6 +34,4 @@ export interface TranslatorOptions {
*/
export interface ITranslator {
translate(key: string, options?: InterpolationOptions): string;
- getUninterpolatedString(key: string): string;
}
-
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 8be459d..bf9b46d 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -2,10 +2,6 @@
* 工具函数模块
*/
-// 文件操作工具
-import * as fileHelpers from './files/fileHelpers';
-export const file = fileHelpers;
-
// 格式化工具
import * as formatHelpers from './format/formatters';
export const format = formatHelpers;
@@ -25,9 +21,6 @@ export const network = {
import * as tokenHelper from './auth/token-helper';
export const auth = tokenHelper;
-// 事件处理工具
-import * as eventEmitter from './events/eventEmitter';
-export const events = eventEmitter;
// 错误管理工具
import { ErrorManager as ErrorManagerClass } from './error';
@@ -45,17 +38,7 @@ export const pdf = {
...pdfPreviewHelper
};
-// 渲染优化工具
-import * as latexOptimizer from './rendering/latexOptimizer';
-export const rendering = latexOptimizer;
-
-// 路由工具
-import * as urlManager from './routing/urlManager';
-export const routing = urlManager;
-// 重试工具
-import * as retryUtils from './retry/retryUtils';
-export const retry = retryUtils;
// 请求管理工具
import { requestManager as requestManagerInstance } from './request/requestManager';
@@ -67,17 +50,11 @@ export const request = {
import * as SmartCacheModule from './cache/SmartCache';
export const cache = SmartCacheModule;
-// 加密和哈希工具
-import * as hashUtils from './crypto/hashUtils';
-export const crypto = hashUtils;
// 内容处理工具
import * as contentFilters from './content';
export const content = contentFilters;
-// 排序工具
-import * as sortingUtils from './sorting/contentSorting';
-export const sorting = sortingUtils;
// 滚动工具
import * as scrollUtils from './scroll/scrollUtils';
@@ -119,34 +96,8 @@ export const performance = {
};
},
- /**
- * 节流函数
- *
- * 限制函数执行频率,在指定时间窗口内最多执行一次。
- *
- * @param func - 要节流的函数
- * @param limit - 时间窗口(毫秒)
- * @returns 节流后的函数
- */
- throttle: unknown>(
- func: F,
- limit: number
- ): ((...args: Parameters) => void) => {
- let inThrottle = false;
-
- return (...args: Parameters): void => {
- if (!inThrottle) {
- func(...args);
- inThrottle = true;
- setTimeout(() => {
- inThrottle = false;
- }, limit);
- }
- };
- }
};
-export type { RetryOptions } from './retry/retryUtils';
export type { SmartCacheOptions } from './cache/SmartCache';
export type { RequestOptions } from './request/requestManager';
export type { ErrorHandlerOptions } from './error/errorHandler';
diff --git a/src/utils/logging/RecorderLogger.ts b/src/utils/logging/RecorderLogger.ts
index e8427d8..27cce19 100644
--- a/src/utils/logging/RecorderLogger.ts
+++ b/src/utils/logging/RecorderLogger.ts
@@ -21,9 +21,6 @@ export class InMemoryLogRecorder implements LogRecorder {
this.buffer.length = 0;
}
- snapshot(): readonly (LoggerArguments & { timestamp: number })[] {
- return [...this.buffer];
- }
}
class RecorderLogger implements Logger {
@@ -77,4 +74,3 @@ export class RecorderLoggerFactory implements LoggerFactory {
return new RecorderLogger(name, this.recorder);
}
}
-
diff --git a/src/utils/logging/logger.ts b/src/utils/logging/logger.ts
index a433e82..15d59c8 100644
--- a/src/utils/logging/logger.ts
+++ b/src/utils/logging/logger.ts
@@ -175,12 +175,3 @@ const createFacade = (name: string): Logger => ({
export const logger = createFacade(APP_LOGGER_NAME);
export const createScopedLogger = (name: string): ReturnType => createFacade(name);
-
-export const getLoggerFactory = (): LoggerFactory => currentFactory;
-
-export const getLogRecorder = (): InMemoryLogRecorder => recorder;
-
-export const registerLoggerReporter = (reporter: ErrorReporter | undefined): void => {
- customReporter = reporter;
- rebuildFactory(developerConfigSnapshot);
-};
diff --git a/src/utils/rendering/latexOptimizer.ts b/src/utils/rendering/latexOptimizer.ts
index 92ab145..703538c 100644
--- a/src/utils/rendering/latexOptimizer.ts
+++ b/src/utils/rendering/latexOptimizer.ts
@@ -147,23 +147,6 @@ export const restoreLatexElements = (): void => {
*
* @returns void
*/
-export const hideLatexElements = (): void => {
- // 使用更激进的方式完全移除元素
- removeLatexElements();
-};
-
-/**
- * 显示所有LaTeX元素
- *
- * 后备方案,调用restoreLatexElements实现。
- *
- * @returns void
- */
-export const showLatexElements = (): void => {
- // 使用批量恢复方法
- restoreLatexElements();
-};
-
/**
* 防抖版的LaTeX元素恢复函数
*/
diff --git a/src/utils/request/requestManager.ts b/src/utils/request/requestManager.ts
index 817fca4..1169fac 100644
--- a/src/utils/request/requestManager.ts
+++ b/src/utils/request/requestManager.ts
@@ -230,34 +230,6 @@ export class RequestManager {
return count;
}
- /**
- * 检查指定的请求是否正在进行
- *
- * @param key - 请求的唯一标识符
- * @returns 如果请求正在进行返回 true
- */
- isPending(key: string): boolean {
- return this.pendingRequests.has(key) || this.debounceTimers.has(key);
- }
-
- /**
- * 获取进行中的请求数量
- *
- * @returns 进行中的请求数量
- */
- getPendingCount(): number {
- return this.pendingRequests.size;
- }
-
- /**
- * 获取所有进行中的请求 key
- *
- * @returns 请求 key 数组
- */
- getPendingKeys(): string[] {
- return Array.from(this.pendingRequests.keys());
- }
-
/**
* 清理资源
*
diff --git a/src/utils/retry/retryUtils.ts b/src/utils/retry/retryUtils.ts
deleted file mode 100644
index ba92611..0000000
--- a/src/utils/retry/retryUtils.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import { logger } from '../logging/logger';
-
-/**
- * 重试选项接口
- */
-export interface RetryOptions {
- /**
- * 最大重试次数
- */
- maxRetries: number;
-
- /**
- * 计算重试延迟的函数
- * @param attempt - 当前重试次数(从0开始)
- * @returns 延迟时间(毫秒)
- */
- backoff: (attempt: number) => number;
-
- /**
- * 判断是否应该重试的函数
- * @param error - 捕获的错误
- * @returns 如果返回true则继续重试,否则直接抛出错误
- */
- shouldRetry?: (error: unknown) => boolean;
-
- /**
- * 重试时的回调函数
- * @param attempt - 当前重试次数
- * @param error - 上次尝试的错误
- */
- onRetry?: (attempt: number, error: unknown) => void;
-
- /**
- * 是否静默重试(不打印日志)
- * 默认为 false
- */
- silent?: boolean;
-}
-
-/**
- * 默认的退避策略:指数退避
- * @param attempt - 重试次数
- * @returns 延迟时间(毫秒)
- */
-export const exponentialBackoff = (attempt: number): number => {
- return Math.min(1000 * Math.pow(2, attempt), 30000); // 最大30秒
-};
-
-/**
- * 固定延迟策略
- * @param delay - 固定延迟时间(毫秒)
- * @returns 返回固定延迟的函数
- */
-export const fixedDelay = (delay: number): ((attempt: number) => number) => {
- return () => delay;
-};
-
-/**
- * 线性退避策略
- * @param initialDelay - 初始延迟时间(毫秒)
- * @param increment - 每次重试增加的延迟时间(毫秒)
- * @returns 返回线性增长延迟的函数
- */
-export const linearBackoff = (initialDelay: number, increment: number): ((attempt: number) => number) => {
- return (attempt: number) => initialDelay + (attempt * increment);
-};
-
-/**
- * 通用重试函数
- *
- * @template T - 返回值类型
- * @param fn - 要执行的异步函数
- * @param options - 重试选项
- * @returns Promise,解析为函数的返回值
- * @throws 当所有重试都失败时抛出最后一个错误
- */
-export async function withRetry(
- fn: () => Promise,
- options: RetryOptions
-): Promise {
- const { silent = false,
- maxRetries,
- backoff,
- shouldRetry = () => true,
- onRetry
- } = options;
-
- let lastError: unknown = null;
-
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
- try {
- // 执行函数
- const result = await fn();
-
- // 成功时记录日志(如果有重试)
- if (attempt > 0) {
- logger.debug(`操作在第 ${String(attempt + 1)} 次尝试后成功`);
- }
-
- return result;
- } catch (error: unknown) {
- lastError = error;
-
- // 检查是否应该重试
- if (attempt < maxRetries && shouldRetry(error)) {
- const delay = backoff(attempt);
-
- // 调用重试回调
- if (onRetry !== undefined) {
- onRetry(attempt, error);
- }
-
- // 记录重试日志(除非设置为静默)
- if (!silent) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.debug(`重试操作 (尝试 ${String(attempt + 1)}/${String(maxRetries + 1)}),延迟 ${String(delay)}ms: ${errorMessage}`);
- }
-
- // 等待指定时间后重试
- await new Promise(resolve => setTimeout(resolve, delay));
- } else {
- // 不应该重试或已达到最大重试次数
- break;
- }
- }
- }
-
- // 所有重试都失败,抛出最后的错误
- throw lastError;
-}
-
-/**
- * 创建一个带重试功能的函数装饰器
- *
- * @param options - 重试选项
- * @returns 返回一个装饰器函数
- */
-export function createRetryDecorator(options: RetryOptions) {
- return function Promise>(
- target: T
- ): T {
- return (async (...args: Parameters) => {
- return withRetry(() => target(...args), options);
- }) as T;
- };
-}
-
-/**
- * 常用的重试配置预设
- */
-export const RetryPresets = {
- /**
- * 快速重试:3次,固定100ms延迟
- */
- fast: {
- maxRetries: 3,
- backoff: fixedDelay(100)
- } as RetryOptions,
-
- /**
- * 标准重试:3次,指数退避
- */
- standard: {
- maxRetries: 3,
- backoff: exponentialBackoff
- } as RetryOptions,
-
- /**
- * 持久重试:5次,指数退避,最大延迟5秒
- */
- persistent: {
- maxRetries: 5,
- backoff: (attempt: number) => Math.min(1000 * Math.pow(2, attempt), 5000)
- } as RetryOptions,
-
- /**
- * 网络请求重试:3次,指数退避,跳过4xx错误
- */
- network: {
- maxRetries: 3,
- backoff: exponentialBackoff,
- shouldRetry: (error: unknown) => {
- if (error instanceof Response) {
- return error.status >= 500 || error.status === 0;
- }
- return true;
- }
- } as RetryOptions
-};
-
-/**
- * 检查错误是否为网络错误
- * @param error - 错误对象
- * @returns 是否为网络错误
- */
-export function isNetworkError(error: unknown): boolean {
- if (error instanceof Error) {
- return error.name === 'NetworkError' ||
- error.name === 'AbortError' ||
- error.message.toLowerCase().includes('network') ||
- error.message.toLowerCase().includes('fetch');
- }
- return false;
-}
-
-/**
- * 检查错误是否为超时错误
- * @param error - 错误对象
- * @returns 是否为超时错误
- */
-export function isTimeoutError(error: unknown): boolean {
- if (error instanceof Error) {
- return error.name === 'TimeoutError' ||
- error.message.toLowerCase().includes('timeout');
- }
- return false;
-}
-
diff --git a/src/utils/routing/urlManager.ts b/src/utils/routing/urlManager.ts
index 847efca..551dcef 100644
--- a/src/utils/routing/urlManager.ts
+++ b/src/utils/routing/urlManager.ts
@@ -155,20 +155,6 @@ function buildUrl(path: string, preview?: string, branch?: string): UrlBuildResu
};
}
-/**
- * 构建包含路径的URL
- *
- * 根据路径、预览参数和分支构建完整URL。
- *
- * @param path - 文件路径
- * @param preview - 预览文件路径(可选)
- * @param branch - 分支名称(可选)
- * @returns 构建的URL字符串(不包含域名)
- */
-export function buildUrlWithParams(path: string, preview?: string, branch?: string): string {
- return buildUrl(path, preview, branch).url;
-}
-
/**
* 更新浏览器URL(不添加历史记录)
*
@@ -226,12 +212,3 @@ export function hasPreviewParam(): boolean {
return false;
}
}
-
-/**
- * 检查URL是否为有效的应用URL
- *
- * @returns 如果是有效的应用URL返回true
- */
-export function isValidAppUrl(): boolean {
- return true; // 所有路径现在都是有效的应用 URL
-}