337 lines
9.0 KiB
TypeScript
337 lines
9.0 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 pollCancelRef = useRef<(() => void) | null>(null);
|
|
const isMountedRef = useRef(true);
|
|
|
|
// 清理轮询
|
|
const clearPolling = useCallback(() => {
|
|
if (pollCancelRef.current) {
|
|
pollCancelRef.current();
|
|
pollCancelRef.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: '任务已提交,开始生成...',
|
|
});
|
|
|
|
// 开始轮询
|
|
const cancelPoll = pollTemplateGeneration(
|
|
generationId,
|
|
handlePollComplete,
|
|
handlePollError
|
|
);
|
|
pollCancelRef.current = cancelPoll;
|
|
|
|
} 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 as any);
|
|
|
|
if (response.data.status === 'completed') {
|
|
handlePollComplete(response.data as any);
|
|
} 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,
|
|
};
|
|
} |