From b2038fc478746d98e3f2bc468eff5606598ead6f Mon Sep 17 00:00:00 2001 From: UE-DND <100979820+UE-DND@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:52:38 +0800 Subject: [PATCH 1/8] 1.3.9.01 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【修复】 - TS 静态错误 - 过时的构建命令 - 潜在的代码问题 【优化】 - Markdown 相对链接跳转稳定性 --- package.json | 2 +- src/components/layout/ReadmeSection.tsx | 26 +++++- .../preview/markdown/MarkdownPreview.tsx | 2 +- src/components/preview/text/TextPreview.tsx | 84 +++++++++++-------- src/services/github/core/Config.ts | 8 +- src/services/github/core/content/service.ts | 13 ++- 6 files changed, 90 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 5d419f0..4c05ab9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev": "vite", "build": "tsc -p scripts/tsconfig.json && node scripts/dist/generateInitialContent.js && node scripts/dist/generateDocfindIndex.js && tsc && vite build", "generate:index": "tsc -p scripts/tsconfig.json && node scripts/dist/generateDocfindIndex.js", - "install-deps": "npm install --legacy-peer-deps", + "install-deps": "npm install", "lint": "eslint . --ext ts,tsx", "preview": "vite preview" }, 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/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/text/TextPreview.tsx b/src/components/preview/text/TextPreview.tsx index 6fff1da..13c060e 100644 --- a/src/components/preview/text/TextPreview.tsx +++ b/src/components/preview/text/TextPreview.tsx @@ -24,20 +24,16 @@ import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; 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 normalizedLines = useMemo(() => { if (typeof content !== "string") { @@ -175,28 +171,6 @@ const TextPreview: React.FC = memo( } }, [prismTheme, theme.palette.text.primary]); - if (loading) { - return ( - - - - ); - } - - if (typeof content !== "string") { - return null; - } - return ( = 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/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/content/service.ts b/src/services/github/core/content/service.ts index 8b48207..2eb086c 100644 --- a/src/services/github/core/content/service.ts +++ b/src/services/github/core/content/service.ts @@ -108,11 +108,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}`); From 2d681ee5bc5ab15d448fa7133470c35b262e85c5 Mon Sep 17 00:00:00 2001 From: UE-DND <100979820+UE-DND@users.noreply.github.com> Date: Wed, 28 Jan 2026 01:16:20 +0800 Subject: [PATCH 2/8] 1.3.9.02 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【优化】 - 移除部分死代码 - README 中弃用的 HTML 特性 --- README.md | 24 +- scripts/generateDocfindIndex.ts | 2 +- scripts/generateInitialContent.ts | 6 +- scripts/src/generated/initialContent.ts | 4 +- src/components/file/FileListItem.tsx | 5 +- src/components/file/FileListRow.tsx | 12 +- .../SearchDrawer/FallbackDialog.tsx | 1 + .../SearchDrawer/FilterSection.tsx | 1 + .../interactions/SearchDrawer/IndexStatus.tsx | 87 +++---- .../interactions/SearchDrawer/SearchInput.tsx | 1 + .../SearchDrawer/SearchResultItem.tsx | 1 + .../SearchDrawer/SearchResults.tsx | 1 + .../interactions/SearchDrawer/index.tsx | 2 +- src/components/layout/ToolbarButtons.tsx | 2 +- .../layout/hooks/useBreadcrumbLayout.ts | 3 +- .../image/hooks/useDesktopNavigation.ts | 6 +- .../preview/image/hooks/useTouchNavigation.ts | 9 +- .../markdown/components/MarkdownCodeBlock.tsx | 2 +- src/components/preview/text/TextPreview.tsx | 2 +- src/components/seo/DynamicSEO.tsx | 2 +- src/components/ui/ErrorBoundary.tsx | 8 +- src/config/core/ConfigLoader.ts | 12 - src/config/core/ConfigManager.ts | 55 ----- src/constants/index.ts | 13 - src/contexts/ColorModeProvider.tsx | 53 ---- src/hooks/github/useReadmeContent.ts | 6 +- src/hooks/github/useRepoSearch/types.ts | 5 +- src/hooks/github/useRepoSearch/utils.ts | 16 +- src/hooks/useFilePreview.ts | 14 +- src/hooks/useThemeTransition.ts | 3 +- src/services/cache/CacheStrategy.ts | 206 ---------------- src/services/configService.ts | 230 ------------------ src/services/github/PatService.ts | 185 -------------- src/services/github/cache/AdvancedCache.ts | 22 +- src/services/github/cache/CacheManager.ts | 33 --- src/services/github/cache/LRUCache.ts | 21 -- src/services/github/config/index.ts | 9 - src/services/github/core/BranchService.ts | 4 +- .../github/core/content/cacheState.ts | 18 -- src/services/github/core/content/service.ts | 1 + src/services/github/schemas/apiSchemas.ts | 65 +---- src/theme/animations.ts | 84 +------ src/theme/g3Curves.ts | 89 +------ src/types/errors.ts | 40 +-- src/types/index.ts | 17 -- src/utils/crypto/hashUtils.ts | 119 --------- src/utils/data-structures/MinHeap.ts | 24 -- src/utils/error/core/ErrorFactory.ts | 129 ---------- src/utils/error/core/ErrorHistory.ts | 60 ----- src/utils/error/core/ErrorLogger.ts | 8 - src/utils/error/core/ErrorManager.ts | 92 +------ src/utils/error/errorHandler.ts | 42 ---- src/utils/events/eventEmitter.ts | 75 ------ src/utils/files/fileHelpers.ts | 25 +- src/utils/format/formatters.ts | 25 +- src/utils/i18n/i18n.ts | 30 --- src/utils/i18n/translator.ts | 9 - src/utils/i18n/types.ts | 2 - src/utils/index.ts | 45 ---- src/utils/logging/logger.ts | 9 - src/utils/rendering/latexOptimizer.ts | 17 -- 61 files changed, 147 insertions(+), 1946 deletions(-) delete mode 100644 src/contexts/ColorModeProvider.tsx delete mode 100644 src/services/cache/CacheStrategy.ts delete mode 100644 src/services/configService.ts delete mode 100644 src/services/github/PatService.ts diff --git a/README.md b/README.md index 9754ac4..aa1a006 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -

- Repo-Viewer -

- -

- 基于 Material Design 3设计风格的 GitHub仓库浏览应用 -    - - Ask DeepWiki - -

+
+

Repo-Viewer

+
+ +
+

+ 基于 Material Design 3设计风格的 GitHub仓库浏览应用 +    + + Ask DeepWiki + +

+
![Preview Dark](docs/image/dark.png) diff --git a/scripts/generateDocfindIndex.ts b/scripts/generateDocfindIndex.ts index e8bf0e1..563b122 100644 --- a/scripts/generateDocfindIndex.ts +++ b/scripts/generateDocfindIndex.ts @@ -402,7 +402,7 @@ const resolveBranchRef = async (repoPath: string, branch: string): Promise(url: string): Promise => { if (!response.ok) { throw new Error(`Request failed: ${response.status} ${response.statusText}`); } - return response.json() as Promise; + return (await response.json()) as T; }; const fetchText = async (url: string): Promise => { @@ -139,7 +139,9 @@ const run = async (): Promise => { const contents = await fetchJson(contentsUrl); if (!Array.isArray(contents)) { - throw new Error("Unexpected contents response"); + console.warn("[hydration] Unexpected contents response, writing null payload."); + await writeOutput(null); + return; } const contentItems = contents.filter(isRecord); diff --git a/scripts/src/generated/initialContent.ts b/scripts/src/generated/initialContent.ts index 59b4eec..cb0ff5c 100644 --- a/scripts/src/generated/initialContent.ts +++ b/scripts/src/generated/initialContent.ts @@ -1,3 +1 @@ -import type { InitialContentHydrationPayload } from "@/types"; - -export const initialContentPayload: InitialContentHydrationPayload | null = null; +export {}; diff --git a/src/components/file/FileListItem.tsx b/src/components/file/FileListItem.tsx index 2e8b72b..da6b67f 100644 --- a/src/components/file/FileListItem.tsx +++ b/src/components/file/FileListItem.tsx @@ -17,7 +17,8 @@ import { Download as DownloadIcon, Cancel as CancelIcon, } from "@mui/icons-material"; -import { file, logger, theme as themeUtils } from "@/utils"; +import { logger, theme as themeUtils } from "@/utils"; +import { fileExtensionIcons } from "@/utils/files/fileHelpers"; import type { GitHubContent } from "@/types"; import { getFeaturesConfig } from "@/config"; import { useI18n } from "@/contexts/I18nContext"; @@ -143,7 +144,7 @@ const FileListItem = memo( const extension = item.name.split('.').pop()?.toLowerCase(); if (typeof extension === "string" && extension.length > 0) { - const icon = file.fileExtensionIcons[extension]; + const icon = fileExtensionIcons[extension]; if (icon !== undefined) { return icon; } diff --git a/src/components/file/FileListRow.tsx b/src/components/file/FileListRow.tsx index 18ca862..4725c9e 100644 --- a/src/components/file/FileListRow.tsx +++ b/src/components/file/FileListRow.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useRef, useState } from "react"; -import type { ReactElement } from "react"; +import { useEffect, useRef, useState } from "react"; +import type { CSSProperties, ReactElement } from "react"; import { motion } from "framer-motion"; import type { MotionStyle } from "framer-motion"; import type { RowComponentProps } from "react-window"; @@ -75,7 +75,7 @@ const RowComponent = ({ const isHighlighted = highlightedIndex === index; - const adjustedStyle: React.CSSProperties = { + const adjustedStyle: CSSProperties = { ...style, boxSizing: "border-box", alignItems: "flex-start", @@ -130,10 +130,4 @@ const RowComponent = ({ ); }; -const Row = React.memo(RowComponent); - -Row.displayName = "FileListRow"; - export { RowComponent }; - -export default Row; 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..cf5c705 100644 --- a/src/components/interactions/SearchDrawer/SearchResultItem.tsx +++ b/src/components/interactions/SearchDrawer/SearchResultItem.tsx @@ -16,6 +16,7 @@ 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"; interface SearchResultItemProps { item: RepoSearchItem; diff --git a/src/components/interactions/SearchDrawer/SearchResults.tsx b/src/components/interactions/SearchDrawer/SearchResults.tsx index c1a76b5..976cf9e 100644 --- a/src/components/interactions/SearchDrawer/SearchResults.tsx +++ b/src/components/interactions/SearchDrawer/SearchResults.tsx @@ -12,6 +12,7 @@ import { g3BorderRadius, G3_PRESETS } from "@/theme/g3Curves"; import { SearchResultItem } from "./SearchResultItem"; import type { RepoSearchItem } from "@/hooks/github/useRepoSearch"; import { useI18n } from "@/contexts/I18nContext"; +import React from "react"; interface SearchResultsProps { items: RepoSearchItem[]; 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/layout/ToolbarButtons.tsx b/src/components/layout/ToolbarButtons.tsx index fa567a3..0ed729b 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, 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/hooks/useDesktopNavigation.ts b/src/components/preview/image/hooks/useDesktopNavigation.ts index ddb28c4..06bf04b 100644 --- a/src/components/preview/image/hooks/useDesktopNavigation.ts +++ b/src/components/preview/image/hooks/useDesktopNavigation.ts @@ -1,5 +1,5 @@ import { useState, useCallback } from 'react'; -import type { RefObject } from 'react'; +import type { MouseEvent, RefObject } from 'react'; interface UseDesktopNavigationOptions { containerRef: RefObject; @@ -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/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/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/text/TextPreview.tsx b/src/components/preview/text/TextPreview.tsx index 13c060e..d15efa9 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, useRef, useState } from "react"; import { Box, CircularProgress, 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/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/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/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/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/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/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/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/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/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/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/service.ts b/src/services/github/core/content/service.ts index 2eb086c..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 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/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..b452fec 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'; @@ -48,22 +47,6 @@ export interface BreadcrumbSegment { path: string; } -/** - * 通知组件扩展选项接口 - */ -export interface OptionsObject extends NotistackOptionsObject { - /** 是否隐藏关闭按钮 */ - hideCloseButton?: boolean; -} - -/** - * 进度通知选项接口 - */ -export interface ProgressSnackbarOptions extends OptionsObject { - /** 进度百分比 */ - progress?: number; -} - /** * 首屏注水的目录条目 */ 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/error/core/ErrorFactory.ts b/src/utils/error/core/ErrorFactory.ts index 3e137d4..c5d226f 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'; @@ -21,13 +19,6 @@ export class ErrorFactory { 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..9efac7b 100644 --- a/src/utils/error/core/ErrorLogger.ts +++ b/src/utils/error/core/ErrorLogger.ts @@ -15,13 +15,6 @@ export class ErrorLogger { 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..04df1ba 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,13 +38,6 @@ 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'; @@ -67,17 +53,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,31 +99,6 @@ 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'; 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元素恢复函数 */ From 53d4a1d942a51e35eda654c7f427d076e5afab74 Mon Sep 17 00:00:00 2001 From: UE-DND <100979820+UE-DND@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:11:46 +0800 Subject: [PATCH 3/8] 1.3.9.03 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【优化】 - 移除部分死代码 --- src/components/layout/ToolbarButtons.tsx | 3 - src/hooks/github/useProgressiveLoading.ts | 120 +----- src/hooks/useErrorHandler.ts | 43 -- src/hooks/useGitHubContentStateMachine.ts | 403 ------------------ src/hooks/useScroll.ts | 60 --- src/services/github.ts | 3 +- src/services/github/RequestBatcher.ts | 34 -- src/services/github/TokenManager.ts | 52 +-- .../github/config/ProxyForceManager.ts | 3 - src/services/github/core/Service.ts | 299 ------------- .../github/core/content/hydrationStore.ts | 2 + src/services/github/proxy/ProxyConfig.ts | 8 - src/theme/components/misc.ts | 1 - src/types/index.ts | 2 - src/types/state.ts | 25 -- src/utils/cache/SmartCache.ts | 71 --- src/utils/content/prismHighlighter.ts | 25 -- src/utils/logging/RecorderLogger.ts | 4 - src/utils/request/requestManager.ts | 28 -- src/utils/retry/retryUtils.ts | 77 ---- src/utils/routing/urlManager.ts | 23 - 21 files changed, 5 insertions(+), 1281 deletions(-) delete mode 100644 src/hooks/useGitHubContentStateMachine.ts delete mode 100644 src/services/github/core/Service.ts delete mode 100644 src/types/state.ts diff --git a/src/components/layout/ToolbarButtons.tsx b/src/components/layout/ToolbarButtons.tsx index 0ed729b..4625523 100644 --- a/src/components/layout/ToolbarButtons.tsx +++ b/src/components/layout/ToolbarButtons.tsx @@ -84,9 +84,6 @@ const ToolbarButtons: React.FC = ({ currentBranch, defaultBranch, currentPath, - branches: _branches, - branchLoading: _branchLoading, - branchError: _branchError, setCurrentBranch, refreshBranches, setCurrentPath, 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/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/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..8334399 100644 --- a/src/hooks/useScroll.ts +++ b/src/hooks/useScroll.ts @@ -174,63 +174,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/services/github.ts b/src/services/github.ts index f0d9d85..97b9a2d 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, }, /** 缓存服务 - 管理缓存和统计 */ diff --git a/src/services/github/RequestBatcher.ts b/src/services/github/RequestBatcher.ts index 68114b2..f2917fa 100644 --- a/src/services/github/RequestBatcher.ts +++ b/src/services/github/RequestBatcher.ts @@ -60,23 +60,6 @@ export class RequestBatcher { return `${method}:${key}:${headerStr}`; } - /** - * 销毁批处理器 - * - * 清理所有定时器和缓存,释放资源。 - * - * @returns void - */ - public destroy(): void { - if (this.batchTimeout !== null && this.batchTimeout !== 0 && !isNaN(this.batchTimeout)) { - clearTimeout(this.batchTimeout); - } - this.batchedRequests.clear(); - this.pendingRequests.clear(); - this.fingerprintWheel.destroy(); - logger.debug('RequestBatcher 已销毁'); - } - /** * 将请求加入批处理队列 * @@ -278,21 +261,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/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/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/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/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/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/types/index.ts b/src/types/index.ts index b452fec..e57d13f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,8 +14,6 @@ export type * from './preview'; // 导出下载相关类型 export type * from './download'; -// 导出状态相关类型 -export type * from './state'; /** * GitHub仓库内容项接口 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/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/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 index ba92611..7e06a31 100644 --- a/src/utils/retry/retryUtils.ts +++ b/src/utils/retry/retryUtils.ts @@ -61,10 +61,6 @@ export const fixedDelay = (delay: number): ((attempt: number) => number) => { * @param increment - 每次重试增加的延迟时间(毫秒) * @returns 返回线性增长延迟的函数 */ -export const linearBackoff = (initialDelay: number, increment: number): ((attempt: number) => number) => { - return (attempt: number) => initialDelay + (attempt * increment); -}; - /** * 通用重试函数 * @@ -135,84 +131,11 @@ export async function withRetry( * @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 -} From fccbc3ae49db0a7d6d0f18c009a838ff49d1541e Mon Sep 17 00:00:00 2001 From: UE-DND <100979820+UE-DND@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:30:39 +0800 Subject: [PATCH 4/8] 1.3.9.04 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【优化】 - 移除部分死代码 --- src/services/github/RequestBatcher.ts | 65 ++++++++++- src/services/github/core/Auth.ts | 46 -------- src/services/github/core/StatsService.ts | 4 +- src/utils/data-structures/TimeWheel.ts | 10 -- src/utils/index.ts | 4 - src/utils/retry/retryUtils.ts | 141 ----------------------- 6 files changed, 63 insertions(+), 207 deletions(-) delete mode 100644 src/utils/retry/retryUtils.ts diff --git a/src/services/github/RequestBatcher.ts b/src/services/github/RequestBatcher.ts index f2917fa..514d5bf 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,6 +21,17 @@ interface FingerprintData { hitCount: number; } +/** + * 重试选项 + */ +interface RetryOptions { + maxRetries: number; + backoff: (attempt: number) => number; + shouldRetry?: (error: unknown) => boolean; + onRetry?: (attempt: number, error: unknown) => void; + silent?: boolean; +} + /** * 请求批处理器类 * @@ -60,6 +70,55 @@ export class RequestBatcher { return `${method}:${key}:${headerStr}`; } + /** + * 重试选项 + */ + 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; + } + } + } + + throw lastError; + }; + /** * 将请求加入批处理队列 * @@ -191,7 +250,7 @@ export class RequestBatcher { try { // 使用通用重试逻辑执行请求 - const result = await retry.withRetry(executeRequest, retryOptions); + const result = await this.withRetry(executeRequest, retryOptions); // 缓存成功的请求结果 if (!skipDeduplication) { diff --git a/src/services/github/core/Auth.ts b/src/services/github/core/Auth.ts index ab6fe20..d890b35 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'; @@ -147,46 +141,6 @@ export function handleApiError(error: Response, endpoint: string, method = 'GET' * @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 管理器实例 * 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/utils/data-structures/TimeWheel.ts b/src/utils/data-structures/TimeWheel.ts index bcbc246..d50d670 100644 --- a/src/utils/data-structures/TimeWheel.ts +++ b/src/utils/data-structures/TimeWheel.ts @@ -281,15 +281,6 @@ export class TimeWheel { }; } - /** - * 销毁时间轮 - * - * 停止时钟并清空所有数据。 - */ - destroy(): void { - this.stop(); - this.clear(); - } } /** @@ -307,4 +298,3 @@ export function createTimeWheel(options?: { wheel.start(); return wheel; } - diff --git a/src/utils/index.ts b/src/utils/index.ts index 04df1ba..bf9b46d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -39,9 +39,6 @@ export const pdf = { }; -// 重试工具 -import * as retryUtils from './retry/retryUtils'; -export const retry = retryUtils; // 请求管理工具 import { requestManager as requestManagerInstance } from './request/requestManager'; @@ -101,7 +98,6 @@ export const performance = { }; -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/retry/retryUtils.ts b/src/utils/retry/retryUtils.ts deleted file mode 100644 index 7e06a31..0000000 --- a/src/utils/retry/retryUtils.ts +++ /dev/null @@ -1,141 +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 返回线性增长延迟的函数 - */ -/** - * 通用重试函数 - * - * @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 返回一个装饰器函数 - */ -/** - * 常用的重试配置预设 - */ -/** - * 检查错误是否为网络错误 - * @param error - 错误对象 - * @returns 是否为网络错误 - */ From 2e56085a7e6369c4401f660616093ff67d3de111 Mon Sep 17 00:00:00 2001 From: UE-DND <100979820+UE-DND@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:06:42 +0800 Subject: [PATCH 5/8] 1.3.9.05 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【优化】 - 移除部分死代码 - 文档/注释语法错误 --- CONTRIBUTING.md | 4 +- README.md | 2 +- src/components/interactions/index.ts | 3 +- .../preview/image/ImagePreviewContent.tsx | 2 +- .../image/hooks/useKeyboardNavigation.ts | 2 +- src/components/preview/image/types.ts | 2 +- .../preview/markdown/styles/markdownStyles.ts | 2 +- .../preview/markdown/utils/imageUtils.ts | 2 +- src/components/preview/text/TextPreview.tsx | 2 +- src/components/ui/index.ts | 6 --- src/contexts/SEOContext/index.tsx | 1 - src/hooks/useCopyToClipboard.ts | 5 +- src/hooks/useDownload.ts | 36 +++++++++----- src/index.css | 47 ------------------- src/services/github.ts | 1 - src/services/github/RequestBatcher.ts | 6 +-- src/services/github/core/Auth.ts | 17 ------- src/services/github/proxy/ProxyService.ts | 10 ++-- src/utils/data-structures/TimeWheel.ts | 11 ----- src/utils/error/core/ErrorFactory.ts | 2 +- src/utils/error/core/ErrorLogger.ts | 2 +- 21 files changed, 47 insertions(+), 118 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 046e00a..5607203 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ npm run lint ### 代码格式化 -我们使用 `EditorConfig` 进行一致的格式化,请确保你的编辑器安装了相应插件以自动应用格式规则。 +我们使用 `EditorConfig` 进行统一格式化,请确保你的编辑器安装了相应插件以自动应用格式规则。 ## 提交 issue @@ -104,7 +104,7 @@ npm run lint ### [enhancement] -- 清晰的增强描述,杜绝抽象概念 +- 清晰地描述增强点,杜绝抽象概念 - 建议的实现逻辑 ### [feature] diff --git a/README.md b/README.md index aa1a006..2d641de 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## 主要功能 -- 📁 **仓库浏览**:直观的文件结构导航,同时提供首页文件和文件夹排除选项. +- 📁 **仓库浏览**:直观的文件结构导航,同时提供首页文件与文件夹排除选项. - 🔎 **文件搜索**:支持基于自建索引和 Github API 的快速文件搜索,可按分支、路径前缀和扩展名过滤. - 📄 **文件预览**:多种文件格式预览,目前支持 `Markdown`、 `PDF` 和 `图片`. - ⬇️ **文件下载**:可下载单个文件或整个文件夹. 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/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 }) => ( <> - {/* 图片内容 */} + {/* 图片展示 */} void) | undefined; /** 切换到下一张图片的回调 */ onNext?: (() => void) | undefined; - /** 初始宽高比(用于占位与渐进过渡) */ + /** 初始宽高比(用于占位和渐进式过渡) */ initialAspectRatio?: number | null; /** 宽高比变更时回调 */ onAspectRatioChange?: (aspectRatio: number) => void; 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 d15efa9..a175476 100644 --- a/src/components/preview/text/TextPreview.tsx +++ b/src/components/preview/text/TextPreview.tsx @@ -63,7 +63,7 @@ const TextPreviewContent: React.FC = memo( const charCount = useMemo(() => (typeof content === "string" ? content.length : 0), [content]); - // 计算实际字节大小(UTF-8编码) + // 计算实际字节大小(UTF-8 编码) const byteSize = useMemo(() => { if (typeof content !== "string") { return 0; 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/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/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/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/github.ts b/src/services/github.ts index 97b9a2d..52a0511 100644 --- a/src/services/github.ts +++ b/src/services/github.ts @@ -97,7 +97,6 @@ export const GitHub = { getAuthHeaders: AuthModule.getAuthHeaders, handleApiError: AuthModule.handleApiError, updateTokenRateLimitFromResponse: AuthModule.updateTokenRateLimitFromResponse, - getTokenManager: AuthModule.getTokenManager, }, /** 代理服务 - 管理代理和图片转换 */ diff --git a/src/services/github/RequestBatcher.ts b/src/services/github/RequestBatcher.ts index 514d5bf..4a9b24f 100644 --- a/src/services/github/RequestBatcher.ts +++ b/src/services/github/RequestBatcher.ts @@ -36,7 +36,7 @@ interface RetryOptions { * 请求批处理器类 * * 管理和优化HTTP请求,提供请求合并、去重、优先级排序和重试机制。 - * 自动批处理相同的请求,减少网络开销并提升性能。 + * 自动批处理重复请求,减少网络开销并提升性能。 */ export class RequestBatcher { private readonly batchedRequests = new Map(); @@ -150,7 +150,7 @@ export class RequestBatcher { skipDeduplication = false } = options; - // 检查是否有相同的请求正在进行 + // 检查是否有重复请求正在进行 if (this.pendingRequests.has(key)) { logger.debug(`请求合并: ${key}`); return this.pendingRequests.get(key) as Promise; @@ -252,7 +252,7 @@ export class RequestBatcher { // 使用通用重试逻辑执行请求 const result = await this.withRetry(executeRequest, retryOptions); - // 缓存成功的请求结果 + // 缓存成功响应的结果 if (!skipDeduplication) { this.fingerprintWheel.add(fingerprint, { result, diff --git a/src/services/github/core/Auth.ts b/src/services/github/core/Auth.ts index d890b35..90578bc 100644 --- a/src/services/github/core/Auth.ts +++ b/src/services/github/core/Auth.ts @@ -132,20 +132,3 @@ export function handleApiError(error: Response, endpoint: string, method = 'GET' return gitHubError; } - -/** - * 标记代理服务失败 - * - * 将指定的代理服务标记为失败状态,触发代理服务切换。 - * - * @param proxyUrl - 失败的代理服务URL - * @returns void - */ -/** - * 获取 Token 管理器实例 - * - * @returns TokenManager 实例 - */ -export function getTokenManager(): GitHubTokenManager { - return tokenManager; -} 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/utils/data-structures/TimeWheel.ts b/src/utils/data-structures/TimeWheel.ts index d50d670..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('时间轮已停止'); - } - } - /** * 添加条目 * diff --git a/src/utils/error/core/ErrorFactory.ts b/src/utils/error/core/ErrorFactory.ts index c5d226f..105c900 100644 --- a/src/utils/error/core/ErrorFactory.ts +++ b/src/utils/error/core/ErrorFactory.ts @@ -13,7 +13,7 @@ import { ErrorLevel, ErrorCategory } from '@/types/errors'; * 负责创建各种类型的结构化错误对象。 */ export class ErrorFactory { - private sessionId: string; + private readonly sessionId: string; constructor(sessionId: string) { this.sessionId = sessionId; diff --git a/src/utils/error/core/ErrorLogger.ts b/src/utils/error/core/ErrorLogger.ts index 9efac7b..859acc9 100644 --- a/src/utils/error/core/ErrorLogger.ts +++ b/src/utils/error/core/ErrorLogger.ts @@ -8,7 +8,7 @@ import { createScopedLogger } from '../../logging/logger'; * 负责将错误记录到控制台或其他日志系统。 */ export class ErrorLogger { - private enableLogging: boolean; + private readonly enableLogging: boolean; private readonly scopedLogger = createScopedLogger('ErrorManager'); constructor(enableLogging = true) { From ce261f6b815bc4c489806f368ba2eda038d3fcc9 Mon Sep 17 00:00:00 2001 From: UE-DND <100979820+UE-DND@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:16:24 +0800 Subject: [PATCH 6/8] 1.3.9.06 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【优化】 - 文件列表项的渲染逻辑 - 文本类型文件预览性能 - 文本类型文件预览响应式设计 --- CONTRIBUTING.md | 2 - README.md | 8 + package-lock.json | 4 +- package.json | 2 +- src/components/file/FileList.tsx | 39 +- src/components/file/FileListRow.tsx | 4 +- src/components/file/utils/fileListLayout.ts | 38 +- src/components/file/utils/types.ts | 2 +- src/components/preview/text/TextPreview.tsx | 386 +++++++++++++------- 9 files changed, 326 insertions(+), 159 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5607203..239de9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,8 +115,6 @@ npm run lint > 推荐保持与历史 issue 一致的标题格式 ---- - ### 许可证 通过贡献,您同意您的贡献将按照与项目相同的许可证 [AGPL-3.0](LICENSE) 进行许可。 diff --git a/README.md b/README.md index 2d641de..2e4ca9f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ +## 为 Repo Viewer 贡献代码 + +此项目已进入稳定阶段,本人将不再花过多精力维护。若发现已知的问题,欢迎任何形式的贡献!无论是修复错误、改进功能,还是提升代码质量,我们都非常欢迎您的参与。 + +> 此组织的所有成员均有管理员权限,若不想提交 Pull Request,直接推送代码是被允许的。 +> +> 但在提交贡献前,推荐阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 以了解建议的代码规范和提交流程。 + ## 主要功能 - 📁 **仓库浏览**:直观的文件结构导航,同时提供首页文件与文件夹排除选项. diff --git a/package-lock.json b/package-lock.json index 514fee8..b22f1e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "repo-viewer", - "version": "1.3.9", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "repo-viewer", - "version": "1.3.9", + "version": "1.4.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/package.json b/package.json index 4c05ab9..1c2eaf7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "repo-viewer", "private": true, - "version": "1.3.9", + "version": "1.4.0", "type": "module", "engines": { "node": "24.x" diff --git a/src/components/file/FileList.tsx b/src/components/file/FileList.tsx index d4ae429..2322d1d 100644 --- a/src/components/file/FileList.tsx +++ b/src/components/file/FileList.tsx @@ -5,7 +5,7 @@ import { AutoSizer } from "react-virtualized-auto-sizer"; import AlphabetIndex from "./AlphabetIndex"; import { RowComponent } from "./FileListRow"; import { FILE_ITEM_CONFIG, LIST_HEIGHT_CONFIG } from "./utils/fileListConfig"; -import { calculateLayoutMetrics } from "./utils/fileListLayout"; +import { calculateLayoutMetrics, getListPadding, getRowMetrics } from "./utils/fileListLayout"; import type { VirtualListItemData, FileListLayoutMetrics } from "./utils/types"; import type { GitHubContent } from "@/types"; import { theme } from "@/utils"; @@ -70,17 +70,8 @@ const FileList = React.memo( // 计算每个文件项的高度(包括间距) // 这个计算需要与 FileListItem 的实际高度保持一致 - const rowHeight = useMemo(() => { - // 基础高度 - const baseHeight = isSmallScreen - ? FILE_ITEM_CONFIG.baseHeight.xs - : FILE_ITEM_CONFIG.baseHeight.sm; - - // 行间距(上下各分一半) - const rowGap = FILE_ITEM_CONFIG.spacing.marginBottom; - - // 计算总高度:基础高度 + 行间距 - return baseHeight + rowGap; + const { rowHeight, rowPaddingBottom } = useMemo(() => { + return getRowMetrics(isSmallScreen); }, [isSmallScreen]); /** @@ -200,6 +191,7 @@ const FileList = React.memo( isScrolling, scrollSpeed, highlightedIndex, + rowPaddingBottom, }), [ contents, @@ -214,26 +206,17 @@ const FileList = React.memo( isScrolling, scrollSpeed, highlightedIndex, + rowPaddingBottom, ], ); // 简化的列表内边距计算 - const listPadding = useMemo((): { paddingTop: number; paddingBottom: number } => { - // 非滚动模式:使用固定的对称内边距,稍微增加一点 - if (!needsScrolling) { - const padding = isSmallScreen ? 16 : 20; - return { - paddingTop: padding - 4, - paddingBottom: padding, - }; - } - - // 滚动模式:使用较小的内边距 - return { - paddingTop: 0, - paddingBottom: 8, - }; - }, [needsScrolling, isSmallScreen]); + // 列表内边距规则集中管理,区分滚动与非滚动模式。 + const listPadding = useMemo( + (): { paddingTop: number; paddingBottom: number } => + getListPadding(needsScrolling, isSmallScreen), + [needsScrolling, isSmallScreen], + ); // 处理滚动事件(监听列表容器) React.useEffect(() => { diff --git a/src/components/file/FileListRow.tsx b/src/components/file/FileListRow.tsx index 4725c9e..0917322 100644 --- a/src/components/file/FileListRow.tsx +++ b/src/components/file/FileListRow.tsx @@ -5,7 +5,6 @@ import type { MotionStyle } from "framer-motion"; import type { RowComponentProps } from "react-window"; import FileListItem from "./FileListItem"; -import { FILE_ITEM_CONFIG } from "./utils/fileListConfig"; import { getDynamicItemVariants, optimizedAnimationStyle } from "./utils/fileListAnimations"; import type { VirtualListItemData } from "./utils/types"; @@ -30,6 +29,7 @@ const RowComponent = ({ isScrolling, scrollSpeed, highlightedIndex, + rowPaddingBottom, } = rowData; const item = contents[index]; @@ -85,7 +85,7 @@ const RowComponent = ({ height: "100%", width: "100%", paddingTop: 0, - paddingBottom: FILE_ITEM_CONFIG.spacing.marginBottom, + paddingBottom: rowPaddingBottom, paddingRight: "12px", boxSizing: "border-box", ...optimizedAnimationStyle, 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/preview/text/TextPreview.tsx b/src/components/preview/text/TextPreview.tsx index a175476..bb9de6d 100644 --- a/src/components/preview/text/TextPreview.tsx +++ b/src/components/preview/text/TextPreview.tsx @@ -1,4 +1,4 @@ -import React, { 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,8 +19,10 @@ 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"; @@ -34,6 +37,11 @@ const TextPreviewContent: React.FC = memo( const { t } = useI18n(); const [wrapText, setWrapText] = useState(false); 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") { @@ -43,23 +51,72 @@ const TextPreviewContent: 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]); @@ -84,6 +141,144 @@ const TextPreviewContent: 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); @@ -128,8 +323,6 @@ const TextPreviewContent: 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; @@ -146,7 +339,7 @@ const TextPreviewContent: React.FC = memo( } }, 0); }); - }, [onClose]); + }, [containerRef, onClose]); useEffect(() => { const container = containerRef.current; @@ -169,7 +362,7 @@ const TextPreviewContent: React.FC = memo( // 更新文本颜色变量 container.style.setProperty('--text-primary', theme.palette.text.primary); } - }, [prismTheme, theme.palette.text.primary]); + }, [containerRef, prismTheme, theme.palette.text.primary]); return ( @@ -177,7 +370,7 @@ const TextPreviewContent: 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)", }, @@ -326,10 +519,11 @@ const TextPreviewContent: React.FC = memo( = memo( }} data-oid="text-preview-header" > - + = memo( - + = memo( data-oid="text-preview-wrap" > {wrapText ? ( - + ) : ( - + )} @@ -400,8 +610,8 @@ const TextPreviewContent: 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, @@ -417,7 +627,11 @@ const TextPreviewContent: React.FC = memo( }} data-oid="text-preview-copy" > - {copied ? : } + {copied ? ( + + ) : ( + + )} @@ -428,100 +642,28 @@ const TextPreviewContent: 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", - }} - /> - - - ))} - - - + /> From 4a1dddac44e38674d222ada1114ce2b8cedd5309 Mon Sep 17 00:00:00 2001 From: UE-DND <100979820+UE-DND@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:45:40 +0800 Subject: [PATCH 7/8] 1.3.9.07 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【优化】 - 搜索高亮性能 --- .../SearchDrawer/SearchResultItem.tsx | 36 +++- .../SearchDrawer/SearchResults.tsx | 179 ++++++++++++++---- .../interactions/SearchDrawer/utils.ts | 55 ++++-- 3 files changed, 216 insertions(+), 54 deletions(-) diff --git a/src/components/interactions/SearchDrawer/SearchResultItem.tsx b/src/components/interactions/SearchDrawer/SearchResultItem.tsx index cf5c705..d008d56 100644 --- a/src/components/interactions/SearchDrawer/SearchResultItem.tsx +++ b/src/components/interactions/SearchDrawer/SearchResultItem.tsx @@ -8,8 +8,6 @@ import { Stack, Tooltip, Typography, - useMediaQuery, - useTheme } from "@mui/material"; import { GitHub as GitHubIcon } from "@mui/icons-material"; import { g3BorderRadius, G3_PRESETS } from "@/theme/g3Curves"; @@ -17,34 +15,56 @@ 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 ( +