mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-15 04:14:42 +08:00
feat(api): implement API configuration validation and error handling in home screen and update section
This commit is contained in:
@@ -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 }) => (
|
||||
<VideoCard
|
||||
id={item.id}
|
||||
source={item.source}
|
||||
@@ -142,6 +152,9 @@ export default function HomeScreen() {
|
||||
return <ActivityIndicator style={{ marginVertical: 20 }} size="large" />;
|
||||
};
|
||||
|
||||
// 检查是否需要显示API配置提示
|
||||
const shouldShowApiConfig = apiConfigStatus.needsConfiguration && selectedCategory && !selectedCategory.tags;
|
||||
|
||||
// TV端和平板端的顶部导航
|
||||
const renderHeader = () => {
|
||||
if (deviceType === "mobile") {
|
||||
@@ -280,8 +293,21 @@ export default function HomeScreen() {
|
||||
{/* 内容网格 */}
|
||||
{shouldShowApiConfig ? (
|
||||
<View style={commonStyles.center}>
|
||||
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: 'center' }}>
|
||||
请点击右上角设置按钮,配置您的服务器地址
|
||||
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: "center" }}>
|
||||
{getApiConfigErrorMessage(apiConfigStatus)}
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : apiConfigStatus.isValidating ? (
|
||||
<View style={commonStyles.center}>
|
||||
<ActivityIndicator size="large" />
|
||||
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: "center" }}>
|
||||
正在验证服务器配置...
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : apiConfigStatus.error && !apiConfigStatus.isValid ? (
|
||||
<View style={commonStyles.center}>
|
||||
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: "center" }}>
|
||||
{apiConfigStatus.error}
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : loading ? (
|
||||
|
||||
@@ -195,7 +195,7 @@ const VideoCard = forwardRef<View, VideoCardProps>(
|
||||
{isContinueWatching && (
|
||||
<View style={styles.infoRow}>
|
||||
<ThemedText style={styles.continueLabel}>
|
||||
第{episodeIndex! + 1}集 已观看 {Math.round((progress || 0) * 100)}%
|
||||
第{episodeIndex}集 已观看 {Math.round((progress || 0) * 100)}%
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
@@ -343,4 +343,4 @@ const styles = StyleSheet.create({
|
||||
color: Colors.dark.primary,
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isLatestVersion && remoteVersion && (
|
||||
<View style={styles.row}>
|
||||
<ThemedText style={styles.label}>状态</ThemedText>
|
||||
<ThemedText style={[styles.value, styles.latestVersion]}>已是最新版本</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<View style={styles.row}>
|
||||
<ThemedText style={styles.label}>检查结果</ThemedText>
|
||||
<ThemedText style={[styles.value, styles.errorText]}>{error}</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{downloading && (
|
||||
<View style={styles.row}>
|
||||
<ThemedText style={styles.label}>下载进度</ThemedText>
|
||||
@@ -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,
|
||||
|
||||
@@ -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`;
|
||||
},
|
||||
|
||||
// 是否显示更新日志
|
||||
|
||||
139
hooks/useApiConfig.ts
Normal file
139
hooks/useApiConfig.ts
Normal file
@@ -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 '加载失败,请重试';
|
||||
};
|
||||
@@ -166,11 +166,11 @@ const useHomeStore = create<HomeState>((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<HomeState>((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 });
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
@@ -43,11 +45,12 @@ export const useUpdateStore = create<UpdateState>((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<UpdateState>((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<UpdateState>((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<UpdateState>((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<UpdateState>((set, get) => ({
|
||||
downloadedPath: null,
|
||||
error: null,
|
||||
showUpdateModal: false,
|
||||
isLatestVersion: false, // 重置时也要重置这个状态
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user