This commit is contained in:
xiamuceer
2025-10-30 11:14:43 +08:00
parent b97410d973
commit 0f6c2d344a
91 changed files with 22309 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
/**
* 事件总线 - 用于跨组件/页面的数据同步通信
*
* 使用方式:
* - eventBus.on('eventName', callback) - 监听事件
* - eventBus.off('eventName', callback) - 取消监听
* - eventBus.emit('eventName', data) - 触发事件
* - eventBus.once('eventName', callback) - 一次性监听
*/
type EventCallback = (data?: unknown) => void;
class EventBus {
private events: Map<string, EventCallback[]> = new Map();
/**
* 监听事件
*/
on(event: string, callback: EventCallback): void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
}
/**
* 取消监听事件
*/
off(event: string, callback: EventCallback): void {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
/**
* 触发事件
*/
emit(event: string, data?: unknown): void {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(cb => {
try {
cb(data);
} catch (error) {
console.error(`事件处理器执行失败 [${event}]:`, error);
}
});
}
}
/**
* 一次性监听事件
*/
once(event: string, callback: EventCallback): void {
const onceCallback: EventCallback = (data) => {
callback(data);
this.off(event, onceCallback);
};
this.on(event, onceCallback);
}
/**
* 移除某个事件的所有监听器
*/
removeAllListeners(event?: string): void {
if (event) {
this.events.delete(event);
} else {
this.events.clear();
}
}
/**
* 获取事件的监听器数量
*/
listenerCount(event: string): number {
return this.events.get(event)?.length || 0;
}
}
// 导出单例
export const eventBus = new EventBus();
// 导出事件名称常量,避免字符串拼写错误
export const EventNames = {
// 项目相关事件
PROJECT_CREATED: 'project:created',
PROJECT_UPDATED: 'project:updated',
PROJECT_DELETED: 'project:deleted',
PROJECT_NEEDS_REFRESH: 'project:needsRefresh',
// 角色相关事件
CHARACTER_CREATED: 'character:created',
CHARACTER_UPDATED: 'character:updated',
CHARACTER_DELETED: 'character:deleted',
CHARACTER_NEEDS_REFRESH: 'character:needsRefresh',
// 大纲相关事件
OUTLINE_CREATED: 'outline:created',
OUTLINE_UPDATED: 'outline:updated',
OUTLINE_DELETED: 'outline:deleted',
OUTLINE_REORDERED: 'outline:reordered',
OUTLINE_GENERATED: 'outline:generated',
OUTLINE_NEEDS_REFRESH: 'outline:needsRefresh',
// 章节相关事件
CHAPTER_CREATED: 'chapter:created',
CHAPTER_UPDATED: 'chapter:updated',
CHAPTER_DELETED: 'chapter:deleted',
CHAPTER_NEEDS_REFRESH: 'chapter:needsRefresh',
} as const;
+400
View File
@@ -0,0 +1,400 @@
/**
* 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]);
// 重排序大纲(带同步)
const reorderOutlines = useCallback(async (orders: Array<{ id: string; order_index: number }>, projectId?: string) => {
try {
await outlineApi.reorderOutlines({ orders });
// 重新获取完整列表以确保顺序正确
const id = projectId || currentProject?.id;
if (id) {
const data = await outlineApi.getOutlines(id);
const outlines = Array.isArray(data) ? data : (data as PaginationResponse<Outline>).items || [];
setOutlines(outlines);
}
} catch (error) {
console.error('重排序大纲失败:', error);
throw error;
}
}, [currentProject?.id, setOutlines]); // 添加 currentProject?.id 到依赖数组
// 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,
reorderOutlines,
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 generateChapterContent = useCallback(async (chapterId: string) => {
try {
const result = await chapterApi.generateChapterContent(chapterId);
// 直接调用 API 更新
const updated = await chapterApi.updateChapter(chapterId, { content: result.content });
updateChapter(chapterId, updated);
return result;
} catch (error) {
console.error('AI生成章节内容失败:', error);
throw error;
}
}, [updateChapter]);
// AI流式生成章节内容(带同步)
const generateChapterContentStream = useCallback(async (
chapterId: string,
onProgress?: (content: string) => void
) => {
try {
// 使用fetch处理流式响应
const response = await fetch(`/api/chapters/${chapterId}/generate-stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
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 = '';
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]);
if (message.type === 'content' && message.content) {
fullContent += message.content;
if (onProgress) {
onProgress(fullContent);
}
} else if (message.type === 'error') {
throw new Error(message.error || '生成失败');
} else if (message.type === 'done') {
// 生成完成,刷新章节数据
await refreshChapters();
return { content: fullContent, word_count: message.word_count };
}
}
} catch (error) {
console.error('解析SSE消息失败:', error);
}
}
}
return { content: fullContent };
} catch (error) {
console.error('AI流式生成章节内容失败:', error);
throw error;
}
}, [refreshChapters]);
return {
refreshChapters,
createChapter,
updateChapter: updateChapterSync,
deleteChapter,
generateChapterContent,
generateChapterContentStream,
};
}
+137
View File
@@ -0,0 +1,137 @@
import { create } from 'zustand';
import type { Project, Outline, Character, Chapter } from '../types';
interface AppState {
currentProject: Project | null;
setCurrentProject: (project: Project | null) => void;
projects: Project[];
setProjects: (projects: Project[]) => void;
addProject: (project: Project) => void;
updateProject: (id: string, project: Partial<Project>) => void;
removeProject: (id: string) => void;
outlines: Outline[];
setOutlines: (outlines: Outline[]) => void;
addOutline: (outline: Outline) => void;
updateOutline: (id: string, outline: Partial<Outline>) => void;
removeOutline: (id: string) => void;
characters: Character[];
setCharacters: (characters: Character[]) => void;
addCharacter: (character: Character) => void;
updateCharacter: (id: string, character: Partial<Character>) => void;
removeCharacter: (id: string) => void;
chapters: Chapter[];
setChapters: (chapters: Chapter[]) => void;
addChapter: (chapter: Chapter) => void;
updateChapter: (id: string, chapter: Partial<Chapter>) => void;
removeChapter: (id: string) => void;
currentChapter: Chapter | null;
setCurrentChapter: (chapter: Chapter | null) => void;
loading: boolean;
setLoading: (loading: boolean) => void;
lastUpdated: {
projects?: number;
outlines?: number;
characters?: number;
chapters?: number;
};
markUpdated: (key: 'projects' | 'outlines' | 'characters' | 'chapters') => void;
clearProjectData: () => void;
}
export const useStore = create<AppState>((set) => ({
currentProject: null,
setCurrentProject: (project) => set({ currentProject: project }),
projects: [],
setProjects: (projects) => set({ projects }),
addProject: (project) => set((state) => ({
projects: [...state.projects, project]
})),
updateProject: (id, updatedProject) => set((state) => ({
projects: state.projects.map((p) =>
p.id === id ? { ...p, ...updatedProject } : p
),
currentProject: state.currentProject?.id === id
? { ...state.currentProject, ...updatedProject }
: state.currentProject,
})),
removeProject: (id) => set((state) => ({
projects: state.projects.filter((p) => p.id !== id),
currentProject: state.currentProject?.id === id ? null : state.currentProject,
})),
outlines: [],
setOutlines: (outlines) => set({ outlines }),
addOutline: (outline) => set((state) => ({
outlines: [...state.outlines, outline]
})),
updateOutline: (id, updatedOutline) => set((state) => ({
outlines: state.outlines.map((o) =>
o.id === id ? { ...o, ...updatedOutline } : o
),
})),
removeOutline: (id) => set((state) => ({
outlines: state.outlines.filter((o) => o.id !== id),
})),
characters: [],
setCharacters: (characters) => set({ characters }),
addCharacter: (character) => set((state) => ({
characters: [...state.characters, character]
})),
updateCharacter: (id, updatedCharacter) => set((state) => ({
characters: state.characters.map((c) =>
c.id === id ? { ...c, ...updatedCharacter } : c
),
})),
removeCharacter: (id) => set((state) => ({
characters: state.characters.filter((c) => c.id !== id),
})),
chapters: [],
setChapters: (chapters) => set({ chapters }),
addChapter: (chapter) => set((state) => ({
chapters: [...state.chapters, chapter]
})),
updateChapter: (id, updatedChapter) => set((state) => ({
chapters: state.chapters.map((c) =>
c.id === id ? { ...c, ...updatedChapter } : c
),
currentChapter: state.currentChapter?.id === id
? { ...state.currentChapter, ...updatedChapter }
: state.currentChapter,
})),
removeChapter: (id) => set((state) => ({
chapters: state.chapters.filter((c) => c.id !== id),
currentChapter: state.currentChapter?.id === id ? null : state.currentChapter,
})),
currentChapter: null,
setCurrentChapter: (chapter) => set({ currentChapter: chapter }),
loading: false,
setLoading: (loading) => set({ loading }),
lastUpdated: {},
markUpdated: (key) => set((state) => ({
lastUpdated: {
...state.lastUpdated,
[key]: Date.now(),
},
})),
clearProjectData: () => set({
outlines: [],
characters: [],
chapters: [],
currentChapter: null,
}),
}));