From c4c31d48b90911e79f8f71a862b6476c7c8f87cd Mon Sep 17 00:00:00 2001 From: HappySummer Date: Sat, 25 Apr 2026 21:28:56 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9B=E4=BE=A7?= =?UTF-8?q?=E6=A0=8F=E6=BB=9A=E5=8A=A8=E8=A1=8C=E4=B8=BA=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=BB=E9=A2=98=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复持久化状态中的主题设置问题,不再强制使用暗色主题 改进发现页面的滚动行为,添加侧栏固定功能 --- package-lock.json | 4 ++-- src/components/DiscoveryView.tsx | 41 ++++++++++++++++++++------------ src/store/useAppStore.ts | 3 +-- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3317fd54..481861a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-stars-manager", - "version": "0.5.2", + "version": "0.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-stars-manager", - "version": "0.5.2", + "version": "0.5.3", "dependencies": { "date-fns": "^3.3.1", "highlight.js": "^11.11.1", diff --git a/src/components/DiscoveryView.tsx b/src/components/DiscoveryView.tsx index 01581d61..947e1489 100644 --- a/src/components/DiscoveryView.tsx +++ b/src/components/DiscoveryView.tsx @@ -420,8 +420,11 @@ export const DiscoveryView: React.FC = React.memo(() => { const [searchInput, setSearchInput] = useState(discoverySearchQuery); const scrollContainerRef = useRef(null); + const sidebarRef = useRef(null); // 工具栏显示状态 const [isToolbarVisible, setIsToolbarVisible] = useState(true); + // 侧栏固定状态 + const [isSidebarFixed, setIsSidebarFixed] = useState(false); const lastScrollY = useRef(0); const scrollTimeoutRef = useRef | null>(null); // 用于在频道切换时直接读取最新滚动位置,避免订阅整个 map 导致 effect 重跑 @@ -622,15 +625,18 @@ export const DiscoveryView: React.FC = React.memo(() => { return date.toLocaleDateString(); }, [t]); - // 处理滚动事件:保存滚动位置并控制工具栏显示 + // 处理滚动事件:保存滚动位置、控制工具栏显示、控制侧栏固定 const handleScroll = useCallback(() => { - if (!scrollContainerRef.current) return; + // 获取页面滚动位置(支持window滚动和元素滚动) + const currentScrollY = window.scrollY || window.pageYOffset || 0; - const currentScrollY = scrollContainerRef.current.scrollTop; - - // 同时更新 ref 和 state,保证频道切换 effect 读取到最新值,且 UI 仍保持响应 - discoveryScrollPositionsRef.current[selectedDiscoveryChannel] = currentScrollY; - setDiscoveryScrollPosition(selectedDiscoveryChannel, currentScrollY); + // 控制侧栏固定:当滚动超过一定距离后固定 + const STICKY_THRESHOLD = 150; // 滚动超过150px后固定侧栏 + if (currentScrollY > STICKY_THRESHOLD) { + setIsSidebarFixed(true); + } else { + setIsSidebarFixed(false); + } // 控制工具栏显示/隐藏 if (scrollTimeoutRef.current) { @@ -650,16 +656,18 @@ export const DiscoveryView: React.FC = React.memo(() => { scrollTimeoutRef.current = setTimeout(() => { setIsToolbarVisible(true); }, 1500); - }, [selectedDiscoveryChannel, setDiscoveryScrollPosition]); + }, []); - // 清理滚动定时器 + // 监听 window 滚动事件 useEffect(() => { + window.addEventListener('scroll', handleScroll, { passive: true }); return () => { + window.removeEventListener('scroll', handleScroll); if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } }; - }, []); + }, [handleScroll]); const handleAnalyzePage = useCallback(async () => { if (!githubToken) return; @@ -857,7 +865,7 @@ export const DiscoveryView: React.FC = React.memo(() => { }, [safeDiscoveryChannels]); return ( -
+
{/* Mobile Tab Navigation */} { language={language} /> -
-
+
Date: Sat, 25 Apr 2026 23:37:42 +0800 Subject: [PATCH 02/12] Update src/store/useAppStore.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/store/useAppStore.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 92b50710..9912a231 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -279,7 +279,10 @@ const normalizePersistedState = ( return { ...currentState, ...safePersisted, - theme: safePersisted.theme || 'dark', + theme: + safePersisted.theme === 'light' || safePersisted.theme === 'dark' + ? safePersisted.theme + : 'dark', repositories, releases, searchResults: repositories, From b1b66f87ed3bf9b399587b5305198c94aa35b1f7 Mon Sep 17 00:00:00 2001 From: HappySummer Date: Sat, 25 Apr 2026 23:45:07 +0800 Subject: [PATCH 03/12] =?UTF-8?q?refactor(DiscoveryView):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84scrollContainerRef?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DiscoveryView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/DiscoveryView.tsx b/src/components/DiscoveryView.tsx index 947e1489..6d79dc09 100644 --- a/src/components/DiscoveryView.tsx +++ b/src/components/DiscoveryView.tsx @@ -885,7 +885,6 @@ export const DiscoveryView: React.FC = React.memo(() => { />
Date: Sun, 26 Apr 2026 00:01:22 +0800 Subject: [PATCH 04/12] =?UTF-8?q?refactor(DiscoveryView):=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20window.scrollY=20=E6=9B=BF=E4=BB=A3=20scrollContain?= =?UTF-8?q?erRef=20=E5=A4=84=E7=90=86=E6=BB=9A=E5=8A=A8=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除对 scrollContainerRef 的依赖,统一使用 window 的滚动位置 API 简化滚动位置保存和恢复逻辑,提高代码一致性 --- src/components/DiscoveryView.tsx | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/components/DiscoveryView.tsx b/src/components/DiscoveryView.tsx index 6d79dc09..432c36da 100644 --- a/src/components/DiscoveryView.tsx +++ b/src/components/DiscoveryView.tsx @@ -584,10 +584,8 @@ export const DiscoveryView: React.FC = React.memo(() => { // 切换频道时恢复滚动位置,并自动加载空数据 useEffect(() => { // 恢复当前频道的滚动位置(从 ref 读取最新值,避免订阅整个 map) - if (scrollContainerRef.current) { - const savedPosition = discoveryScrollPositionsRef.current[selectedDiscoveryChannel] || 0; - scrollContainerRef.current.scrollTop = savedPosition; - } + const savedPosition = discoveryScrollPositionsRef.current[selectedDiscoveryChannel] || 0; + window.scrollTo({ top: savedPosition, behavior: 'auto' }); // 取消持久化后,首次打开或切换到空频道时自动加载 const hasRepos = useAppStore.getState().discoveryRepos[selectedDiscoveryChannel]?.length > 0; @@ -874,11 +872,9 @@ export const DiscoveryView: React.FC = React.memo(() => { if (channel === selectedDiscoveryChannel) { return; } - if (scrollContainerRef.current) { - const scrollTop = scrollContainerRef.current.scrollTop; - discoveryScrollPositionsRef.current[selectedDiscoveryChannel] = scrollTop; - setDiscoveryScrollPosition(selectedDiscoveryChannel, scrollTop); - } + const scrollTop = window.scrollY; + discoveryScrollPositionsRef.current[selectedDiscoveryChannel] = scrollTop; + setDiscoveryScrollPosition(selectedDiscoveryChannel, scrollTop); setSelectedDiscoveryChannel(channel); }} language={language} @@ -899,11 +895,9 @@ export const DiscoveryView: React.FC = React.memo(() => { if (channel === selectedDiscoveryChannel) { return; } - if (scrollContainerRef.current) { - const scrollTop = scrollContainerRef.current.scrollTop; - discoveryScrollPositionsRef.current[selectedDiscoveryChannel] = scrollTop; - setDiscoveryScrollPosition(selectedDiscoveryChannel, scrollTop); - } + const scrollTop = window.scrollY; + discoveryScrollPositionsRef.current[selectedDiscoveryChannel] = scrollTop; + setDiscoveryScrollPosition(selectedDiscoveryChannel, scrollTop); setSelectedDiscoveryChannel(channel); }} onRefreshAll={refreshAll} @@ -1056,7 +1050,6 @@ export const DiscoveryView: React.FC = React.memo(() => { {/* 内容区域 */}
{selectedDiscoveryChannel === 'search' && ( From 091cffb20293517c8a7961dd1378aba613f3a730 Mon Sep 17 00:00:00 2001 From: HappySummer Date: Sun, 26 Apr 2026 00:53:21 +0800 Subject: [PATCH 05/12] =?UTF-8?q?style(ReleaseTimeline):=20=E4=B8=BA?= =?UTF-8?q?=E6=9A=97=E9=BB=91=E6=A8=A1=E5=BC=8F=E6=B7=BB=E5=8A=A0=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E5=92=8C=E8=BE=B9=E6=A1=86=E9=A2=9C=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加暗黑模式下的背景和边框颜色样式,提升夜间使用的视觉体验 --- src/components/ReleaseTimeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index f1b845c8..44514070 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -539,7 +539,7 @@ export const ReleaseTimeline: React.FC = () => { )} {subscribedRepoCount === 0 && ( -
+
From 2192acd082f3e5fff061e2b495374a3606172b29 Mon Sep 17 00:00:00 2001 From: HappySummer Date: Sun, 26 Apr 2026 01:28:03 +0800 Subject: [PATCH 06/12] =?UTF-8?q?style:=20=E7=BB=9F=E4=B8=80=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E9=A2=9C=E8=89=B2=E6=A0=B7=E5=BC=8F=E4=B8=BA=E4=B8=AD?= =?UTF-8?q?=E6=80=A7=E7=81=B0=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整多个组件中的颜色样式,将品牌色和状态色统一为中性灰调色板,提升视觉一致性。主要修改包括: - 替换品牌色和状态色为中性灰 - 统一按钮、图标、卡片等元素的颜色样式 - 优化暗黑模式下的颜色对比度 --- src/components/BackToTop.tsx | 11 +++-- src/components/DiscoverySidebar.tsx | 10 ++--- src/components/DiscoveryView.tsx | 40 +++++++++---------- src/components/ScrollToBottom.tsx | 11 +++-- src/components/SettingsPanel.tsx | 12 +++--- src/components/SubscriptionRepoCard.tsx | 5 +-- src/components/settings/AIConfigPanel.tsx | 13 +++--- src/components/settings/BackendPanel.tsx | 14 +++---- src/components/settings/BackupPanel.tsx | 6 +-- src/components/settings/CategoryPanel.tsx | 6 +-- .../settings/DataManagementPanel.tsx | 20 +++++----- src/components/settings/GeneralPanel.tsx | 6 +-- src/components/settings/WebDAVPanel.tsx | 6 +-- src/components/ui/SliderInput.tsx | 2 +- 14 files changed, 78 insertions(+), 84 deletions(-) diff --git a/src/components/BackToTop.tsx b/src/components/BackToTop.tsx index 5214520f..32bd058c 100644 --- a/src/components/BackToTop.tsx +++ b/src/components/BackToTop.tsx @@ -69,14 +69,13 @@ export const BackToTop: React.FC = () => { fixed z-50 flex items-center justify-center w-12 h-12 - bg-brand-indigo hover:bg-gray-100 dark:bg-white/[0.04] - dark:bg-brand-violet dark:hover:bg-brand-violet/90 - text-white + bg-gray-900 dark:bg-white/[0.06] + text-white dark:text-text-secondary rounded-full shadow-lg hover:shadow-xl - transform transition-[opacity,transform] duration-300 ease-out - hover:scale-110 - focus:outline-none focus:ring-2 focus:ring-brand-violet focus:ring-offset-2 + transform transition-[opacity,transform,background-color] duration-300 ease-out + hover:scale-110 hover:bg-gray-800 dark:hover:bg-white/[0.1] + focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ${isVisible && !readmeModalOpen ? 'opacity-100 translate-y-0 pointer-events-auto' diff --git a/src/components/DiscoverySidebar.tsx b/src/components/DiscoverySidebar.tsx index dec0cdc3..d82f7249 100644 --- a/src/components/DiscoverySidebar.tsx +++ b/src/components/DiscoverySidebar.tsx @@ -3,11 +3,11 @@ import { RefreshCw, Loader2, TrendingUp, Rocket, Crown, Tag, Search } from 'luci import type { DiscoveryChannel, DiscoveryChannelId, DiscoveryChannelIcon } from '../types'; const discoveryChannelIconMap: Record = { - trending: , - rocket: , - star: , - tag: , - search: , + trending: , + rocket: , + star: , + tag: , + search: , }; interface DiscoverySidebarProps { diff --git a/src/components/DiscoveryView.tsx b/src/components/DiscoveryView.tsx index 432c36da..329a2dfd 100644 --- a/src/components/DiscoveryView.tsx +++ b/src/components/DiscoveryView.tsx @@ -41,38 +41,38 @@ import type { } from '../types'; const discoveryChannelIconMap: Record = { - trending: , - rocket: , - star: , - tag: , - search: , + trending: , + rocket: , + star: , + tag: , + search: , }; const discoveryChannelStyleMap: Record = { trending: { - gradient: 'from-blue-500 to-indigo-600', - shadow: 'shadow-blue-500/25', - largeIcon: , + gradient: 'from-gray-200 to-gray-300 dark:from-gray-600 dark:to-gray-700', + shadow: 'shadow-black/[0.08]', + largeIcon: , }, rocket: { - gradient: 'from-orange-500 to-red-600', - shadow: 'shadow-orange-500/25', - largeIcon: , + gradient: 'from-gray-200 to-gray-300 dark:from-gray-600 dark:to-gray-700', + shadow: 'shadow-black/[0.08]', + largeIcon: , }, star: { - gradient: 'from-amber-400 to-yellow-600', - shadow: 'shadow-amber-500/25', - largeIcon: , + gradient: 'from-gray-200 to-gray-300 dark:from-gray-600 dark:to-gray-700', + shadow: 'shadow-black/[0.08]', + largeIcon: , }, tag: { - gradient: 'from-emerald-500 to-teal-600', - shadow: 'shadow-emerald-500/25', - largeIcon: , + gradient: 'from-gray-200 to-gray-300 dark:from-gray-600 dark:to-gray-700', + shadow: 'shadow-black/[0.08]', + largeIcon: , }, search: { - gradient: 'from-violet-500 to-purple-600', - shadow: 'shadow-violet-500/25', - largeIcon: , + gradient: 'from-gray-200 to-gray-300 dark:from-gray-600 dark:to-gray-700', + shadow: 'shadow-black/[0.08]', + largeIcon: , }, }; diff --git a/src/components/ScrollToBottom.tsx b/src/components/ScrollToBottom.tsx index f7b295c5..a424c9ac 100644 --- a/src/components/ScrollToBottom.tsx +++ b/src/components/ScrollToBottom.tsx @@ -80,14 +80,13 @@ export const ScrollToBottom: React.FC = ({ fixed z-[1000] flex items-center justify-center w-12 h-12 - bg-brand-indigo hover:bg-gray-100 dark:bg-white/[0.04] - dark:bg-status-emerald0 dark:hover:bg-brand-indigo - text-white + bg-gray-900 dark:bg-white/[0.06] + text-white dark:text-text-secondary rounded-full shadow-lg hover:shadow-xl - transform transition-[opacity,transform] duration-300 ease-out - hover:scale-110 - focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 + transform transition-[opacity,transform,background-color] duration-300 ease-out + hover:scale-110 hover:bg-gray-800 dark:hover:bg-white/[0.1] + focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ${isVisible && !readmeModalOpen ? 'opacity-100 translate-y-0 pointer-events-auto' diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 28ac9823..6bd98102 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -335,7 +335,7 @@ export const SettingsPanel: React.FC = ({
- +

{t('设置', 'Settings')}

@@ -363,8 +363,8 @@ export const SettingsPanel: React.FC = ({ aria-controls={`settings-tabpanel-${tab.id}`} className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors text-left ${ activeTab === tab.id - ? 'bg-brand-indigo/20 text-gray-700 dark:text-text-secondary dark:bg-brand-indigo/20/30 ' - : 'text-gray-900 dark:text-text-secondary hover:bg-gray-200 dark:hover:bg-white/10' + ? 'bg-gray-100 text-gray-900 dark:bg-white/[0.08] dark:text-text-primary font-medium' + : 'text-gray-700 dark:text-text-secondary hover:bg-gray-100 dark:hover:bg-white/[0.04]' }`} > {tab.icon} @@ -399,7 +399,7 @@ export const SettingsPanel: React.FC = ({ return (
- +

{t('设置', 'Settings')}

@@ -420,8 +420,8 @@ export const SettingsPanel: React.FC = ({ aria-controls={`settings-tabpanel-${tab.id}`} className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-150 text-left ${ activeTab === tab.id - ? 'bg-brand-indigo/20 text-gray-700 dark:text-text-secondary dark:bg-brand-indigo/20/30 ' - : 'text-gray-900 dark:text-text-secondary hover:bg-light-surface dark:hover:bg-white/10' + ? 'bg-gray-100 text-gray-900 dark:bg-white/[0.08] dark:text-text-primary font-medium' + : 'text-gray-700 dark:text-text-secondary hover:bg-light-surface dark:hover:bg-white/[0.04]' }`} > {tab.icon} diff --git a/src/components/SubscriptionRepoCard.tsx b/src/components/SubscriptionRepoCard.tsx index 9ea9d7a6..308f3c90 100644 --- a/src/components/SubscriptionRepoCard.tsx +++ b/src/components/SubscriptionRepoCard.tsx @@ -72,10 +72,7 @@ export const SubscriptionRepoCard: React.FC = ({ repo }; const rankBadgeClass = useMemo(() => { - if (repo.rank === 1) return 'bg-gray-100 dark:bg-white/[0.04] text-gray-700 dark:text-text-secondary '; - if (repo.rank === 2) return 'bg-gray-300 text-gray-900 dark:bg-gray-400 dark:text-text-primary'; - if (repo.rank === 3) return 'bg-gray-100 dark:bg-white/[0.04] text-white dark:text-text-primary'; - return 'bg-light-surface text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary'; + return 'bg-light-surface dark:bg-white/[0.04] text-gray-700 dark:text-text-secondary'; }, [repo.rank]); const platformIconMap = useMemo(() => ({ diff --git a/src/components/settings/AIConfigPanel.tsx b/src/components/settings/AIConfigPanel.tsx index b64f2baf..de425309 100644 --- a/src/components/settings/AIConfigPanel.tsx +++ b/src/components/settings/AIConfigPanel.tsx @@ -394,7 +394,7 @@ Focus on practicality and accurate categorization to help users quickly understa )}

{form.baseUrl && ( -

+

{t('最终请求地址: ', 'Final request URL: ')} {buildFinalApiUrl(form.baseUrl, form.apiType)} @@ -438,7 +438,6 @@ Focus on practicality and accurate categorization to help users quickly understa onChange={(v) => setForm(prev => ({ ...prev, concurrency: v }))} min={1} max={10} - marks={[1, 3, 5, 7, 10]} />

{t('同时进行AI分析的仓库数量 (1-10)', 'Number of repositories to analyze simultaneously (1-10)')} @@ -475,7 +474,7 @@ Focus on practicality and accurate categorization to help users quickly understa

{t('恢复默认提示词', 'Restore Default Prompt')} @@ -546,7 +545,7 @@ Focus on practicality and accurate categorization to help users quickly understa )} {isCustomPromptSameAsDefault && ( - + ({t('默认值', 'Default')}) )} @@ -603,8 +602,8 @@ Focus on practicality and accurate categorization to help users quickly understa key={config.id} className={`p-4 rounded-lg border transition-colors ${ config.id === activeAIConfig - ? 'border-brand-violet bg-brand-indigo/10 dark:border-brand-violet/50 dark:bg-brand-indigo/20' - : 'border-black/[0.06] dark:border-white/[0.04] hover:border-black/[0.06] dark:hover:border-gray-500' + ? 'border-gray-300 bg-gray-50 dark:border-white/[0.12] dark:bg-white/[0.06]' + : 'border-black/[0.06] dark:border-white/[0.04] hover:border-black/[0.06] dark:hover:border-white/[0.08]' }`} >
diff --git a/src/components/settings/BackendPanel.tsx b/src/components/settings/BackendPanel.tsx index e6e22f2a..7c2981e1 100644 --- a/src/components/settings/BackendPanel.tsx +++ b/src/components/settings/BackendPanel.tsx @@ -183,7 +183,7 @@ export const BackendPanel: React.FC = ({ t }) => { const getStatusClass = () => { switch (status) { case 'connected': - return 'bg-status-emerald text-status-emerald '; + return 'bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary'; case 'checking': return 'bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary'; default: @@ -207,14 +207,14 @@ export const BackendPanel: React.FC = ({ t }) => {
{health && ( -
+
- - + + {t('连接正常', 'Connection OK')}
-

+

{t('版本', 'Version')}: {health.version}

@@ -257,7 +257,7 @@ export const BackendPanel: React.FC = ({ t }) => {
- +

{t('同步到后端', 'Sync to Backend')} @@ -283,7 +283,7 @@ export const BackendPanel: React.FC = ({ t }) => {
- +

{t('从后端同步', 'Sync from Backend')} diff --git a/src/components/settings/BackupPanel.tsx b/src/components/settings/BackupPanel.tsx index 411e5305..9516094b 100644 --- a/src/components/settings/BackupPanel.tsx +++ b/src/components/settings/BackupPanel.tsx @@ -256,7 +256,7 @@ export const BackupPanel: React.FC = ({ t }) => { return (
- +

{t('备份与恢复', 'Backup & Restore')}

@@ -290,7 +290,7 @@ export const BackupPanel: React.FC = ({ t }) => {
- +

{t('备份数据', 'Backup Data')} @@ -316,7 +316,7 @@ export const BackupPanel: React.FC = ({ t }) => {
- +

{t('恢复数据', 'Restore Data')} diff --git a/src/components/settings/CategoryPanel.tsx b/src/components/settings/CategoryPanel.tsx index b68ee53b..2b54171b 100644 --- a/src/components/settings/CategoryPanel.tsx +++ b/src/components/settings/CategoryPanel.tsx @@ -263,7 +263,7 @@ export const CategoryPanel: React.FC = ({ t }) => {
- +

{t('折叠侧边栏显示设置', 'Collapsed Sidebar Display')} @@ -271,7 +271,7 @@ export const CategoryPanel: React.FC = ({ t }) => {

{t('设置折叠状态下显示的分类个数', 'Set the number of categories to display when collapsed')}

-

+

{t( '提示:折叠侧边栏仅影响显示,所有分类仍可在展开状态下查看。只显示分类顺序前N个分类。', 'Tip: The collapsed sidebar only affects display; all categories remain accessible when expanded. Only the first N categories in the order are displayed.' @@ -505,7 +505,7 @@ export const CategoryPanel: React.FC = ({ t }) => { @@ -1321,7 +1355,10 @@ export const DataManagementPanel: React.FC = ({ t }) =

{stat.label}

-

+

+ {stat.description} +

+

{stat.count} {t('条记录', 'records')}

@@ -1329,7 +1366,7 @@ export const DataManagementPanel: React.FC = ({ t }) = @@ -548,11 +546,11 @@ const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }> ) : (
{isLoading && ( -
- +
+ - {language === 'zh' ? '加载中...' : 'Loading...'} + {language === 'zh' ? '加载中...' : 'Loading...'}
)} diff --git a/src/components/RepositoryCard.tsx b/src/components/RepositoryCard.tsx index 88002f86..175250df 100644 --- a/src/components/RepositoryCard.tsx +++ b/src/components/RepositoryCard.tsx @@ -279,8 +279,8 @@ const RepositoryCardComponent: React.FC = ({ return; } - if (activeConfig.apiKeyStatus === 'decrypt_failed') { - alert(language === 'zh' ? 'AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.'); + if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') { + alert(language === 'zh' ? 'AI服务的API密钥无法解密或为空,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted or is empty. Please re-enter and save the configuration in settings.'); return; } diff --git a/src/components/RepositoryList.tsx b/src/components/RepositoryList.tsx index 1a7ea5d3..27322f6c 100644 --- a/src/components/RepositoryList.tsx +++ b/src/components/RepositoryList.tsx @@ -277,8 +277,8 @@ export const RepositoryList: React.FC = ({ return; } - if (activeConfig.apiKeyStatus === 'decrypt_failed') { - alert(language === 'zh' ? 'AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.'); + if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') { + alert(language === 'zh' ? 'AI服务的API密钥无法解密或为空,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted or is empty. Please re-enter and save the configuration in settings.'); return; } diff --git a/src/components/SubscriptionRepoCard.tsx b/src/components/SubscriptionRepoCard.tsx index 308f3c90..b57d42c9 100644 --- a/src/components/SubscriptionRepoCard.tsx +++ b/src/components/SubscriptionRepoCard.tsx @@ -209,8 +209,8 @@ export const SubscriptionRepoCard: React.FC = ({ repo return; } - if (activeConfig.apiKeyStatus === 'decrypt_failed') { - alert(t('AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.')); + if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') { + alert(t('AI服务的API密钥无法解密或为空,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted or is empty. Please re-enter and save the configuration in settings.')); return; } diff --git a/src/components/settings/AIConfigPanel.tsx b/src/components/settings/AIConfigPanel.tsx index b7df7033..b90cb6ed 100644 --- a/src/components/settings/AIConfigPanel.tsx +++ b/src/components/settings/AIConfigPanel.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react' import { Bot, Plus, Edit3, Trash2, Save, X, TestTube, RefreshCw, MessageSquare, Eye, EyeOff, AlertCircle } from 'lucide-react'; import { AIConfig, AIApiType, AIReasoningEffort } from '../../types'; import { useAppStore } from '../../store/useAppStore'; -import { AIService } from '../../services/aiService'; +import { AIService, ConnectionTestResult } from '../../services/aiService'; import { buildFinalApiUrl } from '../../utils/apiUrlBuilder'; import { SliderInput } from '../ui/SliderInput'; @@ -152,12 +152,12 @@ export const AIConfigPanel: React.FC = ({ t }) => { setTestingId(config.id); try { const aiService = new AIService(config, language); - const isConnected = await aiService.testConnection(); - - if (isConnected) { + const result = await aiService.testConnection(); + + if (result.success) { alert(t('AI服务连接成功!', 'AI service connection successful!')); } else { - alert(t('AI服务连接失败,请检查配置。', 'AI service connection failed. Please check configuration.')); + alert(result.message); } } catch (error) { console.error('AI test failed:', error); @@ -190,12 +190,12 @@ export const AIConfigPanel: React.FC = ({ t }) => { }; const aiService = new AIService(tempConfig, language); - const isConnected = await aiService.testConnection(); - - if (isConnected) { - alert(t('AI服务连接成功!', 'AI service connection successful!')); + const result = await aiService.testConnection(); + + if (result.success) { + alert(t('✅ AI服务连接成功!', '✅ AI service connection successful!')); } else { - alert(t('AI服务连接失败,请检查配置。', 'AI service connection failed. Please check configuration.')); + alert(result.message); } } catch (error) { console.error('AI test failed:', error); @@ -630,11 +630,11 @@ Focus on practicality and accurate categorization to help users quickly understa {(config.apiType || 'openai').toUpperCase()} • {config.baseUrl} • {config.model} • {t('并发数', 'Concurrency')}: {config.concurrency || 1} {config.reasoningEffort ? ` • reasoning: ${config.reasoningEffort}` : ''}

- {config.apiKeyStatus === 'decrypt_failed' && ( + {(config.apiKeyStatus === 'decrypt_failed' || config.apiKeyStatus === 'empty') && (

{t( - '存储的 API Key 无法解密,请重新输入并保存该配置。', - 'The stored API key could not be decrypted. Please re-enter and save this configuration.' + '存储的 API Key 无法解密或为空,请重新输入并保存该配置。', + 'The stored API key could not be decrypted or is empty. Please re-enter and save this configuration.' )}

)} diff --git a/src/services/aiService.ts b/src/services/aiService.ts index ef5e7b50..876fcf22 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -25,6 +25,38 @@ interface OpenAIResponse { choices?: OpenAIResponseChoice[]; } +export interface ConnectionTestResult { + success: boolean; + statusCode?: number; + statusText?: string; + errorType?: 'network' | 'auth' | 'timeout' | 'server' | 'unknown'; + message: string; +} + +function getStatusCodeMeaning(statusCode: number, language: string): string { + const meanings: Record = { + 400: { zh: '请求参数错误', en: 'Bad Request' }, + 401: { zh: 'API密钥无效或已过期', en: 'Invalid or expired API key' }, + 403: { zh: '没有权限访问该资源', en: 'Forbidden - no permission' }, + 404: { zh: 'API端点或模型不存在', en: 'API endpoint or model not found' }, + 408: { zh: '请求超时', en: 'Request timeout' }, + 429: { zh: '请求过于频繁,已达到速率限制', en: 'Rate limit exceeded' }, + 500: { zh: '服务器内部错误', en: 'Internal server error' }, + 502: { zh: '网关错误,服务器暂时不可用', en: 'Bad Gateway' }, + 503: { zh: '服务暂时不可用,请稍后重试', en: 'Service unavailable' }, + 504: { zh: '网关超时', en: 'Gateway timeout' }, + }; + return meanings[statusCode]?.[language as 'zh' | 'en'] || (language === 'zh' ? '未知错误' : 'Unknown error'); +} + +function getErrorTypeFromStatus(statusCode: number): ConnectionTestResult['errorType'] { + if (statusCode === 401 || statusCode === 403) return 'auth'; + if (statusCode === 408 || statusCode === 504) return 'timeout'; + if (statusCode >= 500) return 'server'; + if (statusCode >= 400) return 'unknown'; + return 'unknown'; +} + export class AIService { private config: AIConfig; private language: string; @@ -442,14 +474,23 @@ Focus on practicality and accurate categorization to help users quickly understa } } - async testConnection(): Promise { + async testConnection(): Promise { + const apiType = this.getApiType(); + const timeoutMs = apiType === 'openai-responses' || this.config.reasoningEffort ? 30000 : 10000; + try { const base = new URL(this.config.baseUrl); - if (base.protocol !== 'http:' && base.protocol !== 'https:') return false; + if (base.protocol !== 'http:' && base.protocol !== 'https:') { + return { + success: false, + errorType: 'unknown', + message: this.language === 'zh' + ? '无效的协议,请使用 http:// 或 https://' + : 'Invalid protocol, please use http:// or https://', + }; + } const controller = new AbortController(); - const apiType = this.getApiType(); - const timeoutMs = apiType === 'openai-responses' || this.config.reasoningEffort ? 30000 : 10000; const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const content = await this.requestText({ @@ -459,12 +500,92 @@ Focus on practicality and accurate categorization to help users quickly understa maxTokens: 50, signal: controller.signal, }); - return !!content; + if (content) { + return { + success: true, + message: this.language === 'zh' ? '连接成功' : 'Connection successful', + }; + } + return { + success: false, + errorType: 'unknown', + message: this.language === 'zh' ? '未收到响应内容' : 'No content received', + }; } finally { clearTimeout(timeoutId); } - } catch { - return false; + } catch (error) { + const err = error as Error; + const errorMessage = err.message || ''; + + // 解析状态码 + const statusMatch = errorMessage.match(/(\d{3})/); + const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined; + + // 处理超时错误 + if (errorMessage.includes('timeout') || errorMessage.includes('abort') || err.name === 'AbortError') { + return { + success: false, + errorType: 'timeout', + message: this.language === 'zh' + ? `连接超时(${timeoutMs / 1000}秒)。请检查:1. 网络连接是否正常 2. API端点是否正确 3. 服务器是否响应缓慢` + : `Connection timeout (${timeoutMs / 1000}s). Please check: 1. Network connection 2. API endpoint 3. Server response time`, + }; + } + + // 处理网络错误 + if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('Failed to fetch')) { + return { + success: false, + errorType: 'network', + message: this.language === 'zh' + ? '网络连接失败。请检查:1. 网络连接是否正常 2. API端点地址是否正确 3. 防火墙或代理设置' + : 'Network connection failed. Please check: 1. Network connection 2. API endpoint 3. Firewall or proxy settings', + }; + } + + // 如果有状态码,提供详细的错误信息 + if (statusCode) { + const meaning = getStatusCodeMeaning(statusCode, this.language); + const errorType = getErrorTypeFromStatus(statusCode) ?? 'unknown'; + const suggestions: Record = { + auth: { + zh: '请检查 API 密钥是否正确,或密钥是否已过期', + en: 'Please check if the API key is correct or expired', + }, + timeout: { + zh: '请求超时,请稍后重试或检查网络连接', + en: 'Request timeout, please retry later or check network', + }, + server: { + zh: '服务器端错误,请稍后重试或联系服务提供商', + en: 'Server error, please retry later or contact provider', + }, + unknown: { + zh: '请检查 API 端点、模型名称和请求参数是否正确', + en: 'Please check API endpoint, model name and request parameters', + }, + }; + + return { + success: false, + statusCode, + statusText: meaning, + errorType, + message: this.language === 'zh' + ? `HTTP ${statusCode} - ${meaning}\n建议:${suggestions[errorType].zh}` + : `HTTP ${statusCode} - ${meaning}\nSuggestion: ${suggestions[errorType].en}`, + }; + } + + // 默认错误 + return { + success: false, + errorType: 'unknown', + message: this.language === 'zh' + ? `连接失败:${errorMessage || '未知错误'}\n请检查 API 端点、API 密钥和模型名称是否正确` + : `Connection failed: ${errorMessage || 'Unknown error'}\nPlease check API endpoint, API key and model name`, + }; } } From 37cb28cadeba387c7893471a0bae4ebbfae22e5a Mon Sep 17 00:00:00 2001 From: HappySummer <141414769+SummerRay160@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:55:11 +0800 Subject: [PATCH 12/12] Update src/components/settings/AIConfigPanel.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/components/settings/AIConfigPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/AIConfigPanel.tsx b/src/components/settings/AIConfigPanel.tsx index b90cb6ed..18c1f29d 100644 --- a/src/components/settings/AIConfigPanel.tsx +++ b/src/components/settings/AIConfigPanel.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react' import { Bot, Plus, Edit3, Trash2, Save, X, TestTube, RefreshCw, MessageSquare, Eye, EyeOff, AlertCircle } from 'lucide-react'; import { AIConfig, AIApiType, AIReasoningEffort } from '../../types'; import { useAppStore } from '../../store/useAppStore'; -import { AIService, ConnectionTestResult } from '../../services/aiService'; +import { AIService } from '../../services/aiService'; import { buildFinalApiUrl } from '../../utils/apiUrlBuilder'; import { SliderInput } from '../ui/SliderInput';