Files
MuMuAINovel/frontend/src/store/hooks.ts
T

409 lines
12 KiB
TypeScript
Raw Normal View History

2025-10-30 11:14:43 +08:00
/**
* Store Hooks - 提供数据获取和自动同步功能
* 这些 hooks 封装了数据获取逻辑,并自动更新 store
*/
import { useCallback } from 'react';
import { message } from 'antd';
import { useStore } from './index';
import { projectApi, outlineApi, characterApi, chapterApi } from '../services/api';
import type {
PaginationResponse,
Outline,
Character,
Chapter,
Project,
ProjectCreate,
ProjectUpdate,
OutlineCreate,
OutlineUpdate,
ChapterCreate,
ChapterUpdate,
GenerateOutlineRequest,
GenerateCharacterRequest
} from '../types';
/**
* 项目数据同步 Hook
*/
export function useProjectSync() {
const { setProjects, setLoading, addProject, updateProject, removeProject } = useStore();
// 刷新项目列表
const refreshProjects = useCallback(async () => {
try {
setLoading(true);
const data = await projectApi.getProjects();
const projects = Array.isArray(data) ? data : (data as PaginationResponse<Project>).items || [];
setProjects(projects);
return projects;
} catch (error) {
console.error('刷新项目列表失败:', error);
message.error('刷新项目列表失败');
return [];
} finally {
setLoading(false);
}
}, [setProjects, setLoading]);
// 创建项目(带同步)
const createProject = useCallback(async (data: ProjectCreate) => {
try {
const created = await projectApi.createProject(data);
addProject(created);
return created;
} catch (error) {
console.error('创建项目失败:', error);
throw error;
}
}, [addProject]);
// 更新项目(带同步)
const updateProjectSync = useCallback(async (id: string, data: ProjectUpdate) => {
try {
const updated = await projectApi.updateProject(id, data);
updateProject(id, updated);
return updated;
} catch (error) {
console.error('更新项目失败:', error);
throw error;
}
}, [updateProject]);
// 删除项目(带同步)
const deleteProject = useCallback(async (id: string) => {
try {
await projectApi.deleteProject(id);
removeProject(id);
} catch (error) {
console.error('删除项目失败:', error);
throw error;
}
}, [removeProject]);
return {
refreshProjects,
createProject,
updateProject: updateProjectSync,
deleteProject,
};
}
/**
* 角色数据同步 Hook
*/
export function useCharacterSync() {
const { currentProject, setCharacters, addCharacter, removeCharacter } = useStore();
// 刷新角色列表
const refreshCharacters = useCallback(async (projectId?: string) => {
const id = projectId || currentProject?.id;
if (!id) return [];
try {
const data = await characterApi.getCharacters(id);
const characters = Array.isArray(data) ? data : (data as PaginationResponse<Character>).items || [];
setCharacters(characters);
return characters;
} catch (error) {
console.error('刷新角色列表失败:', error);
message.error('刷新角色列表失败');
return [];
}
}, [currentProject?.id, setCharacters]);
// 删除角色(带同步)
const deleteCharacter = useCallback(async (id: string) => {
try {
await characterApi.deleteCharacter(id);
removeCharacter(id);
} catch (error) {
console.error('删除角色失败:', error);
throw error;
}
}, [removeCharacter]);
// AI生成角色(带同步)
const generateCharacter = useCallback(async (data: GenerateCharacterRequest) => {
try {
const generated = await characterApi.generateCharacter(data);
addCharacter(generated);
return generated;
} catch (error) {
console.error('AI生成角色失败:', error);
throw error;
}
}, [addCharacter]);
return {
refreshCharacters,
deleteCharacter,
generateCharacter,
};
}
/**
* 大纲数据同步 Hook
*/
export function useOutlineSync() {
const { currentProject, setOutlines, addOutline, updateOutline, removeOutline } = useStore();
// 刷新大纲列表
const refreshOutlines = useCallback(async (projectId?: string) => {
const id = projectId || currentProject?.id;
if (!id) return [];
try {
const data = await outlineApi.getOutlines(id);
const outlines = Array.isArray(data) ? data : (data as PaginationResponse<Outline>).items || [];
setOutlines(outlines);
return outlines;
} catch (error) {
console.error('刷新大纲列表失败:', error);
message.error('刷新大纲列表失败');
return [];
}
}, [currentProject?.id, setOutlines]); // 添加 currentProject?.id 到依赖数组
// 创建大纲(带同步)
const createOutline = useCallback(async (data: OutlineCreate) => {
try {
const created = await outlineApi.createOutline(data);
addOutline(created);
return created;
} catch (error) {
console.error('创建大纲失败:', error);
throw error;
}
}, [addOutline]);
// 更新大纲(带同步)
const updateOutlineSync = useCallback(async (id: string, data: OutlineUpdate) => {
try {
const updated = await outlineApi.updateOutline(id, data);
updateOutline(id, updated);
return updated;
} catch (error) {
console.error('更新大纲失败:', error);
throw error;
}
}, [updateOutline]);
// 删除大纲(带同步)
const deleteOutline = useCallback(async (id: string) => {
try {
await outlineApi.deleteOutline(id);
removeOutline(id);
} catch (error) {
console.error('删除大纲失败:', error);
throw error;
}
}, [removeOutline]);
// AI生成大纲(带同步)
const generateOutlines = useCallback(async (data: GenerateOutlineRequest) => {
try {
const result = await outlineApi.generateOutline(data);
const outlines = Array.isArray(result) ? result : (result as PaginationResponse<Outline>).items || [];
outlines.forEach((outline: Outline) => addOutline(outline));
return outlines;
} catch (error) {
console.error('AI生成大纲失败:', error);
throw error;
}
}, [addOutline]);
return {
refreshOutlines,
createOutline,
updateOutline: updateOutlineSync,
deleteOutline,
generateOutlines,
};
}
/**
* 章节数据同步 Hook
*/
export function useChapterSync() {
const { currentProject, setChapters, addChapter, updateChapter, removeChapter } = useStore();
// 刷新章节列表
const refreshChapters = useCallback(async (projectId?: string) => {
const id = projectId || currentProject?.id;
if (!id) return [];
try {
const data = await chapterApi.getChapters(id);
const chapters = Array.isArray(data) ? data : (data as PaginationResponse<Chapter>).items || [];
setChapters(chapters);
return chapters;
} catch (error) {
console.error('刷新章节列表失败:', error);
message.error('刷新章节列表失败');
return [];
}
}, [currentProject?.id, setChapters]); // 添加 currentProject?.id 到依赖数组
// 创建章节(带同步)
const createChapter = useCallback(async (data: ChapterCreate) => {
try {
const created = await chapterApi.createChapter(data);
addChapter(created);
return created;
} catch (error) {
console.error('创建章节失败:', error);
throw error;
}
}, [addChapter]);
// 更新章节(带同步)
const updateChapterSync = useCallback(async (id: string, data: ChapterUpdate) => {
try {
const updated = await chapterApi.updateChapter(id, data);
updateChapter(id, updated);
return updated;
} catch (error) {
console.error('更新章节失败:', error);
throw error;
}
}, [updateChapter]);
// 删除章节(带同步)
const deleteChapter = useCallback(async (id: string) => {
try {
await chapterApi.deleteChapter(id);
removeChapter(id);
} catch (error) {
console.error('删除章节失败:', error);
throw error;
}
}, [removeChapter]);
// AI流式生成章节内容(带同步)
const generateChapterContentStream = useCallback(async (
chapterId: string,
2025-10-31 17:23:25 +08:00
onProgress?: (content: string) => void,
styleId?: number,
2025-11-14 10:24:53 +08:00
targetWordCount?: number,
onProgressUpdate?: (message: string, progress: number) => void,
model?: string,
narrativePerspective?: string
2025-10-30 11:14:43 +08:00
) => {
try {
// 使用fetch处理流式响应
const response = await fetch(`/api/chapters/${chapterId}/generate-stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
style_id: styleId,
target_word_count: targetWordCount,
model: model,
narrative_perspective: narrativePerspective
}),
2025-10-30 11:14:43 +08:00
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('无法获取响应流');
}
let buffer = '';
let fullContent = '';
2025-11-05 00:11:27 +08:00
let analysisTaskId: string | undefined;
2025-10-30 11:14:43 +08:00
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
// 处理缓冲区中的完整消息
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '' || line.startsWith(':')) {
continue;
}
try {
const dataMatch = line.match(/^data: (.+)$/m);
if (dataMatch) {
const message = JSON.parse(dataMatch[1]);
2025-11-14 10:24:53 +08:00
if (message.type === 'start') {
// 开始生成
if (onProgressUpdate) {
onProgressUpdate(message.message || '开始生成...', 0);
}
} else if (message.type === 'progress') {
// 进度更新
if (onProgressUpdate) {
onProgressUpdate(
message.message || '生成中...',
message.progress || 0
);
}
} else if ((message.type === 'content' || message.type === 'chunk') && message.content) {
2025-10-30 11:14:43 +08:00
fullContent += message.content;
if (onProgress) {
onProgress(fullContent);
}
} else if (message.type === 'error') {
throw new Error(message.error || '生成失败');
} else if (message.type === 'done') {
2025-11-05 00:11:27 +08:00
// 生成完成,保存分析任务ID
analysisTaskId = message.analysis_task_id;
2025-11-14 10:24:53 +08:00
if (onProgressUpdate) {
onProgressUpdate('生成完成', 100);
}
2025-10-30 11:14:43 +08:00
// 生成完成,刷新章节数据
await refreshChapters();
2025-11-14 10:24:53 +08:00
} else if (message.type === 'analysis_started') {
// 分析已开始
analysisTaskId = message.task_id;
if (onProgressUpdate) {
onProgressUpdate('章节分析已开始...', 100);
}
2025-11-05 00:11:27 +08:00
} else if (message.type === 'analysis_queued') {
// 分析任务已加入队列
analysisTaskId = message.task_id;
2025-10-30 11:14:43 +08:00
}
}
} catch (error) {
console.error('解析SSE消息失败:', error);
}
}
}
2025-11-05 00:11:27 +08:00
return {
content: fullContent,
analysis_task_id: analysisTaskId
};
2025-10-30 11:14:43 +08:00
} catch (error) {
console.error('AI流式生成章节内容失败:', error);
throw error;
}
}, [refreshChapters]);
return {
refreshChapters,
createChapter,
updateChapter: updateChapterSync,
deleteChapter,
generateChapterContentStream,
};
}