From 10a806a6573a0aa88a8abbb86ecd55749804afeb Mon Sep 17 00:00:00 2001 From: zimplexing Date: Thu, 14 Aug 2025 14:08:11 +0800 Subject: [PATCH] feat(api): implement API configuration validation and error handling in home screen and update section --- app/index.tsx | 70 +++++++++---- components/VideoCard.tv.tsx | 4 +- components/settings/UpdateSection.tsx | 34 ++++++- constants/UpdateConfig.ts | 4 +- hooks/useApiConfig.ts | 139 ++++++++++++++++++++++++++ stores/homeStore.ts | 24 ++++- stores/updateStore.ts | 23 ++++- 7 files changed, 263 insertions(+), 35 deletions(-) create mode 100644 hooks/useApiConfig.ts diff --git a/app/index.tsx b/app/index.tsx index a1e25de..aa3489e 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -10,11 +10,11 @@ import { Search, Settings, LogOut, Heart } from "lucide-react-native"; import { StyledButton } from "@/components/StyledButton"; import useHomeStore, { RowItem, Category } from "@/stores/homeStore"; import useAuthStore from "@/stores/authStore"; -import { useSettingsStore } from "@/stores/settingsStore"; import CustomScrollView from "@/components/CustomScrollView"; import { useResponsiveLayout } from "@/hooks/useResponsiveLayout"; import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles"; import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation"; +import { useApiConfig, getApiConfigErrorMessage } from "@/hooks/useApiConfig"; const LOAD_MORE_THRESHOLD = 200; @@ -44,7 +44,7 @@ export default function HomeScreen() { clearError, } = useHomeStore(); const { isLoggedIn, logout } = useAuthStore(); - const { apiBaseUrl } = useSettingsStore(); + const apiConfigStatus = useApiConfig(); useFocusEffect( useCallback(() => { @@ -52,34 +52,44 @@ export default function HomeScreen() { }, [refreshPlayRecords]) ); + // 统一的数据获取逻辑 useEffect(() => { - // 只有在 apiBaseUrl 存在时才调用 fetchInitialData,避免时序问题 - if (selectedCategory && !selectedCategory.tags && apiBaseUrl) { - fetchInitialData(); - } else if (selectedCategory?.tags && !selectedCategory.tag) { + if (!selectedCategory) return; + + // 如果是容器分类且没有选择标签,设置默认标签 + if (selectedCategory.tags && !selectedCategory.tag) { const defaultTag = selectedCategory.tags[0]; setSelectedTag(defaultTag); selectCategory({ ...selectedCategory, tag: defaultTag }); + return; } - }, [selectedCategory, fetchInitialData, selectCategory, apiBaseUrl]); - useEffect(() => { - // 只有在 apiBaseUrl 存在时才调用 fetchInitialData,避免时序问题 - if (selectedCategory && selectedCategory.tag && apiBaseUrl) { - fetchInitialData(); + // 只有在API配置完成且分类有效时才获取数据 + if (apiConfigStatus.isConfigured && !apiConfigStatus.needsConfiguration) { + // 对于有标签的分类,需要确保有标签才获取数据 + if (selectedCategory.tags && selectedCategory.tag) { + fetchInitialData(); + } + // 对于无标签的分类,直接获取数据 + else if (!selectedCategory.tags) { + fetchInitialData(); + } } - }, [fetchInitialData, selectedCategory, selectedCategory.tag, apiBaseUrl]); + }, [ + selectedCategory, + selectedCategory?.tag, + apiConfigStatus.isConfigured, + apiConfigStatus.needsConfiguration, + fetchInitialData, + selectCategory, + ]); - // 检查是否需要显示API配置提示 - const shouldShowApiConfig = !apiBaseUrl && selectedCategory && !selectedCategory.tags; - - // 清除错误状态,当API未配置时 + // 清除错误状态的逻辑 useEffect(() => { - if (shouldShowApiConfig && error) { - // 如果需要显示API配置提示,清除之前的错误状态 + if (apiConfigStatus.needsConfiguration && error) { clearError(); } - }, [shouldShowApiConfig, error, clearError]); + }, [apiConfigStatus.needsConfiguration, error, clearError]); useEffect(() => { if (!loading && contentData.length > 0) { @@ -119,7 +129,7 @@ export default function HomeScreen() { ); }; - const renderContentItem = ({ item, index }: { item: RowItem; index: number }) => ( + const renderContentItem = ({ item }: { item: RowItem; index: number }) => ( ; }; + // 检查是否需要显示API配置提示 + const shouldShowApiConfig = apiConfigStatus.needsConfiguration && selectedCategory && !selectedCategory.tags; + // TV端和平板端的顶部导航 const renderHeader = () => { if (deviceType === "mobile") { @@ -280,8 +293,21 @@ export default function HomeScreen() { {/* 内容网格 */} {shouldShowApiConfig ? ( - - 请点击右上角设置按钮,配置您的服务器地址 + + {getApiConfigErrorMessage(apiConfigStatus)} + + + ) : apiConfigStatus.isValidating ? ( + + + + 正在验证服务器配置... + + + ) : apiConfigStatus.error && !apiConfigStatus.isValid ? ( + + + {apiConfigStatus.error} ) : loading ? ( diff --git a/components/VideoCard.tv.tsx b/components/VideoCard.tv.tsx index a953fde..eb7882f 100644 --- a/components/VideoCard.tv.tsx +++ b/components/VideoCard.tv.tsx @@ -195,7 +195,7 @@ const VideoCard = forwardRef( {isContinueWatching && ( - 第{episodeIndex! + 1}集 已观看 {Math.round((progress || 0) * 100)}% + 第{episodeIndex}集 已观看 {Math.round((progress || 0) * 100)}% )} @@ -343,4 +343,4 @@ const styles = StyleSheet.create({ color: Colors.dark.primary, fontSize: 12, }, -}); \ No newline at end of file +}); diff --git a/components/settings/UpdateSection.tsx b/components/settings/UpdateSection.tsx index ddf6f9a..99e63e1 100644 --- a/components/settings/UpdateSection.tsx +++ b/components/settings/UpdateSection.tsx @@ -6,8 +6,16 @@ import { useUpdateStore } from "@/stores/updateStore"; // import { UPDATE_CONFIG } from "@/constants/UpdateConfig"; export function UpdateSection() { - const { currentVersion, remoteVersion, updateAvailable, downloading, downloadProgress, checkForUpdate } = - useUpdateStore(); + const { + currentVersion, + remoteVersion, + updateAvailable, + downloading, + downloadProgress, + checkForUpdate, + isLatestVersion, + error + } = useUpdateStore(); const [checking, setChecking] = React.useState(false); @@ -36,6 +44,20 @@ export function UpdateSection() { )} + {isLatestVersion && remoteVersion && ( + + 状态 + 已是最新版本 + + )} + + {error && ( + + 检查结果 + {error} + + )} + {downloading && ( 下载进度 @@ -96,6 +118,14 @@ const styles = StyleSheet.create({ color: "#00bb5e", fontWeight: "bold", }, + latestVersion: { + color: "#00bb5e", + fontWeight: "500", + }, + errorText: { + color: "#ff6b6b", + fontWeight: "500", + }, buttonContainer: { flexDirection: "row", gap: 12, diff --git a/constants/UpdateConfig.ts b/constants/UpdateConfig.ts index 72f15b7..688e351 100644 --- a/constants/UpdateConfig.ts +++ b/constants/UpdateConfig.ts @@ -7,11 +7,11 @@ export const UPDATE_CONFIG = { // GitHub相关URL GITHUB_RAW_URL: - "https://ghfast.top/https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json", + "https://gh-proxy.com/https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json", // 获取平台特定的下载URL getDownloadUrl(version: string): string { - return `https://ghfast.top/https://github.com/zimplexing/OrionTV/releases/download/v${version}/orionTV.${version}.apk`; + return `https://gh-proxy.com/https://github.com/zimplexing/OrionTV/releases/download/v${version}/orionTV.${version}.apk`; }, // 是否显示更新日志 diff --git a/hooks/useApiConfig.ts b/hooks/useApiConfig.ts new file mode 100644 index 0000000..0f870ff --- /dev/null +++ b/hooks/useApiConfig.ts @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +import { useSettingsStore } from '@/stores/settingsStore'; +import { api } from '@/services/api'; + +export interface ApiConfigStatus { + isConfigured: boolean; + isValidating: boolean; + isValid: boolean | null; + error: string | null; + needsConfiguration: boolean; +} + +export const useApiConfig = () => { + const { apiBaseUrl, serverConfig, isLoadingServerConfig } = useSettingsStore(); + const [validationState, setValidationState] = useState<{ + isValidating: boolean; + isValid: boolean | null; + error: string | null; + }>({ + isValidating: false, + isValid: null, + error: null, + }); + + const isConfigured = Boolean(apiBaseUrl && apiBaseUrl.trim()); + const needsConfiguration = !isConfigured; + + // Validate API configuration when it changes + useEffect(() => { + if (!isConfigured) { + setValidationState({ + isValidating: false, + isValid: false, + error: null, + }); + return; + } + + const validateConfig = async () => { + setValidationState(prev => ({ ...prev, isValidating: true, error: null })); + + try { + await api.getServerConfig(); + setValidationState({ + isValidating: false, + isValid: true, + error: null, + }); + } catch (error) { + let errorMessage = '服务器连接失败'; + + if (error instanceof Error) { + switch (error.message) { + case 'API_URL_NOT_SET': + errorMessage = 'API地址未设置'; + break; + case 'UNAUTHORIZED': + errorMessage = '服务器认证失败'; + break; + default: + if (error.message.includes('Network')) { + errorMessage = '网络连接失败,请检查网络或服务器地址'; + } else if (error.message.includes('timeout')) { + errorMessage = '连接超时,请检查服务器地址'; + } else if (error.message.includes('404')) { + errorMessage = '服务器地址无效,请检查API路径'; + } else if (error.message.includes('500')) { + errorMessage = '服务器内部错误'; + } + break; + } + } + + setValidationState({ + isValidating: false, + isValid: false, + error: errorMessage, + }); + } + }; + + // Only validate if not already loading server config + if (!isLoadingServerConfig) { + validateConfig(); + } + }, [apiBaseUrl, isConfigured, isLoadingServerConfig]); + + // Reset validation when server config loading state changes + useEffect(() => { + if (isLoadingServerConfig) { + setValidationState(prev => ({ ...prev, isValidating: true, error: null })); + } + }, [isLoadingServerConfig]); + + // Update validation state based on server config + useEffect(() => { + if (!isLoadingServerConfig && isConfigured) { + if (serverConfig) { + setValidationState(prev => ({ ...prev, isValid: true, error: null })); + } else { + setValidationState(prev => ({ + ...prev, + isValid: false, + error: prev.error || '无法获取服务器配置' + })); + } + } + }, [serverConfig, isLoadingServerConfig, isConfigured]); + + const status: ApiConfigStatus = { + isConfigured, + isValidating: validationState.isValidating || isLoadingServerConfig, + isValid: validationState.isValid, + error: validationState.error, + needsConfiguration, + }; + + return status; +}; + +export const getApiConfigErrorMessage = (status: ApiConfigStatus): string => { + if (status.needsConfiguration) { + return '请点击右上角设置按钮,配置您的服务器地址'; + } + + if (status.error) { + return status.error; + } + + if (status.isValidating) { + return '正在验证服务器配置...'; + } + + if (status.isValid === false) { + return '服务器配置验证失败,请检查设置'; + } + + return '加载失败,请重试'; +}; \ No newline at end of file diff --git a/stores/homeStore.ts b/stores/homeStore.ts index bc808d8..cb22752 100644 --- a/stores/homeStore.ts +++ b/stores/homeStore.ts @@ -166,11 +166,11 @@ const useHomeStore = create((set, get) => ({ if (pageStart === 0) { // 缓存新数据 dataCache.set(cacheKey, newItems); - set((state) => ({ + set({ contentData: newItems, pageStart: result.list.length, hasMore: true, - })); + }); } else { // 增量加载时不缓存,直接追加 set((state) => ({ @@ -187,11 +187,25 @@ const useHomeStore = create((set, get) => ({ set({ hasMore: false }); } } catch (err: any) { + let errorMessage = "加载失败,请重试"; + if (err.message === "API_URL_NOT_SET") { - set({ error: "请点击右上角设置按钮,配置您的服务器地址" }); - } else { - set({ error: "加载失败,请重试" }); + errorMessage = "请点击右上角设置按钮,配置您的服务器地址"; + } else if (err.message === "UNAUTHORIZED") { + errorMessage = "认证失败,请重新登录"; + } else if (err.message.includes("Network")) { + errorMessage = "网络连接失败,请检查网络连接"; + } else if (err.message.includes("timeout")) { + errorMessage = "请求超时,请检查网络或服务器状态"; + } else if (err.message.includes("404")) { + errorMessage = "服务器API路径不正确,请检查服务器配置"; + } else if (err.message.includes("500")) { + errorMessage = "服务器内部错误,请联系管理员"; + } else if (err.message.includes("403")) { + errorMessage = "访问被拒绝,请检查权限设置"; } + + set({ error: errorMessage }); } finally { set({ loading: false, loadingMore: false }); } diff --git a/stores/updateStore.ts b/stores/updateStore.ts index 3b9a138..738774f 100644 --- a/stores/updateStore.ts +++ b/stores/updateStore.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import updateService from '../services/updateService'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import Toast from 'react-native-toast-message'; interface UpdateState { // 状态 @@ -15,6 +16,7 @@ interface UpdateState { lastCheckTime: number; skipVersion: string | null; showUpdateModal: boolean; + isLatestVersion: boolean; // 新增:是否已是最新版本 // 操作 checkForUpdate: (silent?: boolean) => Promise; @@ -43,11 +45,12 @@ export const useUpdateStore = create((set, get) => ({ lastCheckTime: 0, skipVersion: null, showUpdateModal: false, + isLatestVersion: false, // 新增:初始为false // 检查更新 checkForUpdate: async (silent = false) => { try { - set({ error: null }); + set({ error: null, isLatestVersion: false }); // 获取跳过的版本 const skipVersion = await AsyncStorage.getItem(STORAGE_KEYS.SKIP_VERSION); @@ -58,6 +61,9 @@ export const useUpdateStore = create((set, get) => ({ // 如果有更新且不是要跳过的版本 const shouldShowUpdate = isUpdateAvailable && versionInfo.version !== skipVersion; + // 检查是否已经是最新版本 + const isLatest = !isUpdateAvailable; + set({ remoteVersion: versionInfo.version, downloadUrl: versionInfo.downloadUrl, @@ -65,8 +71,19 @@ export const useUpdateStore = create((set, get) => ({ lastCheckTime: Date.now(), skipVersion, showUpdateModal: shouldShowUpdate && !silent, + isLatestVersion: isLatest, }); + // 如果是手动检查且已是最新版本,显示提示 + if (!silent && isLatest) { + Toast.show({ + type: 'success', + text1: '已是最新版本', + text2: `当前版本 v${updateService.getCurrentVersion()} 已是最新版本`, + visibilityTime: 3000, + }); + } + // 保存最后检查时间 await AsyncStorage.setItem( STORAGE_KEYS.LAST_CHECK_TIME, @@ -76,7 +93,8 @@ export const useUpdateStore = create((set, get) => ({ // console.info('检查更新失败:', error); set({ error: error instanceof Error ? error.message : '检查更新失败', - updateAvailable: false + updateAvailable: false, + isLatestVersion: false, }); } }, @@ -166,6 +184,7 @@ export const useUpdateStore = create((set, get) => ({ downloadedPath: null, error: null, showUpdateModal: false, + isLatestVersion: false, // 重置时也要重置这个状态 }); }, }));