bw-expo-app/hooks/use-template-run.ts

336 lines
8.9 KiB
TypeScript

import { useState, useCallback, useRef } from 'react';
import { Alert } from 'react-native';
import {
runTemplate,
getTemplateGeneration,
pollTemplateGeneration
} from '@/lib/api/template-runs';
import {
RunTemplateData,
TemplateGeneration,
RunProgress,
GenerationStatus
} from '@/lib/types/template-run';
type UseTemplateRunState = 'idle' | 'submitting' | 'running' | 'completed' | 'error';
interface UseTemplateRunOptions {
onSuccess?: (result: TemplateGeneration) => void;
onError?: (error: Error) => void;
onProgress?: (progress: RunProgress) => void;
}
export function useTemplateRun(options: UseTemplateRunOptions = {}) {
const [state, setState] = useState<UseTemplateRunState>('idle');
const [progress, setProgress] = useState<RunProgress>({
status: 'pending',
progress: 0,
message: '准备开始...',
});
const [result, setResult] = useState<TemplateGeneration | null>(null);
const [error, setError] = useState<Error | null>(null);
const pollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isMountedRef = useRef(true);
// 清理轮询
const clearPolling = useCallback(() => {
if (pollTimeoutRef.current) {
clearTimeout(pollTimeoutRef.current);
pollTimeoutRef.current = null;
}
}, []);
// 更新进度状态
const updateProgress = useCallback((generation: TemplateGeneration) => {
if (!isMountedRef.current) return;
let progressPercent = 0;
let message = '处理中...';
switch (generation.status) {
case 'pending':
progressPercent = 10;
message = '任务已提交,等待开始...';
break;
case 'running':
progressPercent = 50;
message = '正在生成内容,请稍候...';
break;
case 'completed':
progressPercent = 100;
message = '生成完成!';
break;
case 'failed':
progressPercent = 0;
message = '生成失败,请重试';
break;
}
const newProgress: RunProgress = {
status: generation.status,
progress: progressPercent,
message,
result: generation,
};
setProgress(newProgress);
options.onProgress?.(newProgress);
}, [options.onProgress]);
// 处理轮询完成
const handlePollComplete = useCallback((generation: TemplateGeneration) => {
if (!isMountedRef.current) return;
clearPolling();
setResult(generation);
setState('completed');
updateProgress(generation);
options.onSuccess?.(generation);
}, [clearPolling, updateProgress, options.onSuccess]);
// 处理轮询错误
const handlePollError = useCallback((error: Error) => {
if (!isMountedRef.current) return;
clearPolling();
setError(error);
setState('error');
options.onError?.(error);
}, [clearPolling, options.onError]);
// 运行模板
const executeTemplate = useCallback(async (
templateId: string,
data: RunTemplateData,
identifier?: string
) => {
if (state === 'submitting' || state === 'running') {
console.warn('模板正在运行中,请勿重复提交');
return;
}
// 检查是否提供了支付凭证
if (!identifier) {
const error = new Error('缺少支付凭证,请先完成支付流程');
setError(error);
setState('error');
setProgress({
status: 'failed',
progress: 0,
message: '支付凭证缺失',
});
options.onError?.(error);
Alert.alert('错误', '支付凭证缺失,无法创建生成任务');
return;
}
try {
// 重置状态
setState('submitting');
setError(null);
setResult(null);
setProgress({
status: 'pending',
progress: 0,
message: '正在提交任务...',
});
// 提交模板运行,传递支付凭证
const runResponse = await runTemplate(templateId, data, identifier);
if (!runResponse.success || !runResponse.data) {
throw new Error('提交任务失败');
}
const generationId = runResponse.data;
setState('running');
setProgress({
status: 'pending',
progress: 10,
message: '任务已提交,开始生成...',
});
// 开始轮询
pollTemplateGeneration(
generationId,
handlePollComplete,
handlePollError
);
} catch (error) {
const err = error as Error;
console.error('运行模板失败:', err);
clearPolling();
setError(err);
setState('error');
setProgress({
status: 'failed',
progress: 0,
message: err.message || '运行失败',
});
options.onError?.(err);
}
}, [state, clearPolling, handlePollComplete, handlePollError, options.onError]);
// 手动刷新状态
const refreshStatus = useCallback(async (generationId: string) => {
try {
const response = await getTemplateGeneration(generationId);
if (response.success && response.data) {
updateProgress(response.data);
if (response.data.status === 'completed') {
handlePollComplete(response.data);
} else if (response.data.status === 'failed') {
handlePollError(new Error('任务执行失败'));
}
}
} catch (error) {
console.error('刷新状态失败:', error);
}
}, [updateProgress, handlePollComplete, handlePollError]);
// 取消运行
const cancelRun = useCallback(() => {
if (state === 'submitting' || state === 'running') {
clearPolling();
setState('idle');
setProgress({
status: 'pending',
progress: 0,
message: '已取消',
});
Alert.alert('提示', '任务已取消');
}
}, [state, clearPolling]);
// 重置状态
const reset = useCallback(() => {
clearPolling();
setState('idle');
setProgress({
status: 'pending',
progress: 0,
message: '准备开始...',
});
setResult(null);
setError(null);
}, [clearPolling]);
// 重试
const retry = useCallback((templateId: string, data: RunTemplateData) => {
reset();
executeTemplate(templateId, data);
}, [reset, executeTemplate]);
// 组件卸载时清理
const cleanup = useCallback(() => {
isMountedRef.current = false;
clearPolling();
}, [clearPolling]);
return {
// 状态
state,
progress,
result,
error,
isLoading: state === 'submitting' || state === 'running',
isCompleted: state === 'completed',
isError: state === 'error',
// 操作
executeTemplate,
refreshStatus,
cancelRun,
reset,
retry,
cleanup,
};
}
// 便捷的 Hook 用于表单数据管理
export function useTemplateFormData(initialData: RunTemplateData = {}) {
const [formData, setFormData] = useState<RunTemplateData>(initialData);
const [errors, setErrors] = useState<Record<string, string>>({});
const updateField = useCallback((fieldName: string, value: any) => {
setFormData(prev => ({ ...prev, [fieldName]: value }));
// 清除该字段的错误
if (errors[fieldName]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[fieldName];
return newErrors;
});
}
}, [errors]);
const setFieldError = useCallback((fieldName: string, error: string) => {
setErrors(prev => ({ ...prev, [fieldName]: error }));
}, []);
const clearErrors = useCallback(() => {
setErrors({});
}, []);
const resetForm = useCallback(() => {
setFormData(initialData);
clearErrors();
}, [initialData, clearErrors]);
const validateForm = useCallback((validation: Record<string, any> = {}) => {
const newErrors: Record<string, string> = {};
let isValid = true;
Object.entries(validation).forEach(([fieldName, rules]) => {
const value = formData[fieldName];
// 检查必填
if (rules.required && (value === undefined || value === null || value === '')) {
newErrors[fieldName] = `${fieldName} 是必填项`;
isValid = false;
return;
}
// 如果值为空且不是必填,跳过其他验证
if (value === undefined || value === null || value === '') {
return;
}
// 其他验证规则
if (rules.min && typeof value === 'string' && value.length < rules.min) {
newErrors[fieldName] = `${fieldName} 至少需要 ${rules.min} 个字符`;
isValid = false;
}
if (rules.max && typeof value === 'string' && value.length > rules.max) {
newErrors[fieldName] = `${fieldName} 最多 ${rules.max} 个字符`;
isValid = false;
}
if (rules.pattern && typeof value === 'string' && !new RegExp(rules.pattern).test(value)) {
newErrors[fieldName] = rules.message || `${fieldName} 格式不正确`;
isValid = false;
}
});
setErrors(newErrors);
return { isValid, errors: newErrors };
}, [formData]);
return {
formData,
errors,
updateField,
setFieldError,
clearErrors,
resetForm,
validateForm,
isValid: Object.keys(errors).length === 0,
};
}