From fb16cc4072219957759363ac3a5466b9ea25c203 Mon Sep 17 00:00:00 2001 From: xiamuceer-j Date: Wed, 14 Jan 2026 19:47:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AF=BC=E5=85=A5=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA=EF=BC=9A=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8D=87=E7=BA=A7=E8=87=B31.1.0=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=81=8C=E4=B8=9A=E7=B3=BB=E7=BB=9F=E3=80=81=E6=95=85?= =?UTF-8?q?=E4=BA=8B=E8=AE=B0=E5=BF=86=E3=80=81=E5=89=A7=E6=83=85=E5=88=86?= =?UTF-8?q?=E6=9E=90=E7=9A=84=E5=AF=BC=E5=87=BA=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/schemas/import_export.py | 95 ++- backend/app/services/import_export_service.py | 553 +++++++++++++++++- frontend/src/pages/ProjectList.tsx | 93 ++- 3 files changed, 707 insertions(+), 34 deletions(-) diff --git a/backend/app/schemas/import_export.py b/backend/app/schemas/import_export.py index d936fa2..f6a86c1 100644 --- a/backend/app/schemas/import_export.py +++ b/backend/app/schemas/import_export.py @@ -8,6 +8,9 @@ class ExportOptions(BaseModel): """导出选项""" include_generation_history: bool = Field(False, description="是否包含生成历史") include_writing_styles: bool = Field(True, description="是否包含写作风格") + include_careers: bool = Field(True, description="是否包含职业系统") + include_memories: bool = Field(False, description="是否包含故事记忆(数据量可能较大)") + include_plot_analysis: bool = Field(False, description="是否包含剧情分析") class ChapterExportData(BaseModel): @@ -118,9 +121,93 @@ class GenerationHistoryExportData(BaseModel): created_at: Optional[str] = None +class CareerExportData(BaseModel): + """职业导出数据""" + name: str + type: str # main/sub + description: Optional[str] = None + category: Optional[str] = None + stages: str # JSON格式的阶段列表 + max_stage: int = 10 + requirements: Optional[str] = None + special_abilities: Optional[str] = None + worldview_rules: Optional[str] = None + attribute_bonuses: Optional[str] = None + source: str = "ai" + created_at: Optional[str] = None + + +class CharacterCareerExportData(BaseModel): + """角色职业关联导出数据""" + character_name: str # 通过名称关联 + career_name: str # 通过名称关联 + career_type: str # main/sub + current_stage: int = 1 + stage_progress: int = 0 + started_at: Optional[str] = None + reached_current_stage_at: Optional[str] = None + notes: Optional[str] = None + + +class StoryMemoryExportData(BaseModel): + """故事记忆导出数据""" + chapter_title: Optional[str] = None # 通过章节标题关联 + memory_type: str + title: Optional[str] = None + content: str + full_context: Optional[str] = None + related_characters: Optional[List[str]] = None # 角色名称列表 + related_locations: Optional[List[str]] = None + tags: Optional[List[str]] = None + importance_score: float = 0.5 + story_timeline: int + chapter_position: int = 0 + text_length: int = 0 + is_foreshadow: int = 0 + foreshadow_strength: Optional[float] = None + created_at: Optional[str] = None + + +class PlotAnalysisExportData(BaseModel): + """剧情分析导出数据""" + chapter_title: str # 通过章节标题关联 + plot_stage: Optional[str] = None + conflict_level: Optional[int] = None + conflict_types: Optional[List[str]] = None + emotional_tone: Optional[str] = None + emotional_intensity: Optional[float] = None + emotional_curve: Optional[Dict[str, float]] = None + hooks: Optional[List[Dict[str, Any]]] = None + hooks_count: int = 0 + hooks_avg_strength: Optional[float] = None + foreshadows: Optional[List[Dict[str, Any]]] = None + foreshadows_planted: int = 0 + foreshadows_resolved: int = 0 + plot_points: Optional[List[Dict[str, Any]]] = None + plot_points_count: int = 0 + character_states: Optional[List[Dict[str, Any]]] = None + scenes: Optional[List[Dict[str, Any]]] = None + pacing: Optional[str] = None + overall_quality_score: Optional[float] = None + pacing_score: Optional[float] = None + engagement_score: Optional[float] = None + coherence_score: Optional[float] = None + analysis_report: Optional[str] = None + suggestions: Optional[List[str]] = None + word_count: Optional[int] = None + dialogue_ratio: Optional[float] = None + description_ratio: Optional[float] = None + created_at: Optional[str] = None + + +class ProjectDefaultStyleExportData(BaseModel): + """项目默认风格导出数据""" + style_name: str # 通过风格名称关联 + + class ProjectExportData(BaseModel): """项目完整导出数据""" - version: str = "1.0.0" + version: str = "1.1.0" # 升级版本号 export_time: str project: Dict[str, Any] chapters: List[ChapterExportData] = [] @@ -131,6 +218,12 @@ class ProjectExportData(BaseModel): organization_members: List[OrganizationMemberExportData] = [] writing_styles: List[WritingStyleExportData] = [] generation_history: List[GenerationHistoryExportData] = [] + # 新增字段 + careers: List[CareerExportData] = [] + character_careers: List[CharacterCareerExportData] = [] + story_memories: List[StoryMemoryExportData] = [] + plot_analysis: List[PlotAnalysisExportData] = [] + project_default_style: Optional[ProjectDefaultStyleExportData] = None class ImportValidationResult(BaseModel): diff --git a/backend/app/services/import_export_service.py b/backend/app/services/import_export_service.py index c27a7e1..f6afb4a 100644 --- a/backend/app/services/import_export_service.py +++ b/backend/app/services/import_export_service.py @@ -3,7 +3,7 @@ import json from datetime import datetime from typing import Dict, List, Optional, Tuple, Any from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select +from sqlalchemy import select, or_ from app.models.project import Project from app.models.chapter import Chapter from app.models.character import Character @@ -11,6 +11,9 @@ from app.models.outline import Outline from app.models.relationship import CharacterRelationship, Organization, OrganizationMember from app.models.writing_style import WritingStyle from app.models.generation_history import GenerationHistory +from app.models.career import Career, CharacterCareer +from app.models.memory import StoryMemory, PlotAnalysis +from app.models.project_default_style import ProjectDefaultStyle from app.schemas.import_export import ( ProjectExportData, ChapterExportData, @@ -21,6 +24,11 @@ from app.schemas.import_export import ( OrganizationMemberExportData, WritingStyleExportData, GenerationHistoryExportData, + CareerExportData, + CharacterCareerExportData, + StoryMemoryExportData, + PlotAnalysisExportData, + ProjectDefaultStyleExportData, ImportValidationResult, ImportResult ) @@ -32,14 +40,18 @@ logger = get_logger(__name__) class ImportExportService: """导入导出服务类""" - SUPPORTED_VERSION = "1.0.0" + SUPPORTED_VERSIONS = ["1.0.0", "1.1.0"] # 支持的版本列表 + CURRENT_VERSION = "1.1.0" # 当前导出版本 @staticmethod async def export_project( project_id: str, db: AsyncSession, include_generation_history: bool = False, - include_writing_styles: bool = True + include_writing_styles: bool = True, + include_careers: bool = True, + include_memories: bool = False, + include_plot_analysis: bool = False ) -> ProjectExportData: """ 导出项目完整数据 @@ -49,6 +61,9 @@ class ImportExportService: db: 数据库会话 include_generation_history: 是否包含生成历史 include_writing_styles: 是否包含写作风格 + include_careers: 是否包含职业系统 + include_memories: 是否包含故事记忆 + include_plot_analysis: 是否包含剧情分析 Returns: ProjectExportData: 导出的项目数据 @@ -77,7 +92,7 @@ class ImportExportService: "chapter_count": project.chapter_count, "narrative_perspective": project.narrative_perspective, "character_count": project.character_count, - "outline_mode": project.outline_mode, + "outline_mode": project.outline_mode, "user_id": project.user_id, "created_at": project.created_at.isoformat() if project.created_at else None, } @@ -118,8 +133,34 @@ class ImportExportService: generation_history = await ImportExportService._export_generation_history(project_id, db) logger.info(f"导出生成历史数: {len(generation_history)}") + # 导出职业系统(可选) + careers = [] + character_careers = [] + if include_careers: + careers = await ImportExportService._export_careers(project_id, db) + logger.info(f"导出职业数: {len(careers)}") + character_careers = await ImportExportService._export_character_careers(project_id, db) + logger.info(f"导出角色职业关联数: {len(character_careers)}") + + # 导出故事记忆(可选) + story_memories = [] + if include_memories: + story_memories = await ImportExportService._export_story_memories(project_id, db) + logger.info(f"导出故事记忆数: {len(story_memories)}") + + # 导出剧情分析(可选) + plot_analysis = [] + if include_plot_analysis: + plot_analysis = await ImportExportService._export_plot_analysis(project_id, db) + logger.info(f"导出剧情分析数: {len(plot_analysis)}") + + # 导出项目默认风格 + project_default_style = await ImportExportService._export_project_default_style(project_id, db) + if project_default_style: + logger.info(f"导出项目默认风格: {project_default_style.style_name}") + export_data = ProjectExportData( - version=ImportExportService.SUPPORTED_VERSION, + version=ImportExportService.CURRENT_VERSION, export_time=datetime.utcnow().isoformat(), project=project_data, chapters=chapters, @@ -129,7 +170,12 @@ class ImportExportService: organizations=organizations, organization_members=org_members, writing_styles=writing_styles, - generation_history=generation_history + generation_history=generation_history, + careers=careers, + character_careers=character_careers, + story_memories=story_memories, + plot_analysis=plot_analysis, + project_default_style=project_default_style ) logger.info(f"项目导出完成: {project_id}") @@ -394,6 +440,185 @@ class ImportExportService: for history, chapter in histories ] + @staticmethod + async def _export_careers(project_id: str, db: AsyncSession) -> List[CareerExportData]: + """导出职业系统""" + result = await db.execute( + select(Career) + .where(Career.project_id == project_id) + .order_by(Career.type, Career.created_at) + ) + careers = result.scalars().all() + + return [ + CareerExportData( + name=career.name, + type=career.type, + description=career.description, + category=career.category, + stages=career.stages, + max_stage=career.max_stage or 10, + requirements=career.requirements, + special_abilities=career.special_abilities, + worldview_rules=career.worldview_rules, + attribute_bonuses=career.attribute_bonuses, + source=career.source or "ai", + created_at=career.created_at.isoformat() if career.created_at else None + ) + for career in careers + ] + + @staticmethod + async def _export_character_careers(project_id: str, db: AsyncSession) -> List[CharacterCareerExportData]: + """导出角色职业关联""" + # 查询所有属于该项目的角色职业关联 + result = await db.execute( + select(CharacterCareer, Character, Career) + .join(Character, CharacterCareer.character_id == Character.id) + .join(Career, CharacterCareer.career_id == Career.id) + .where(Character.project_id == project_id) + ) + character_careers = result.all() + + return [ + CharacterCareerExportData( + character_name=char.name, + career_name=career.name, + career_type=cc.career_type, + current_stage=cc.current_stage or 1, + stage_progress=cc.stage_progress or 0, + started_at=cc.started_at, + reached_current_stage_at=cc.reached_current_stage_at, + notes=cc.notes + ) + for cc, char, career in character_careers + ] + + @staticmethod + async def _export_story_memories(project_id: str, db: AsyncSession) -> List[StoryMemoryExportData]: + """导出故事记忆""" + # 构建章节ID到标题的映射 + chapter_result = await db.execute( + select(Chapter).where(Chapter.project_id == project_id) + ) + chapters = chapter_result.scalars().all() + chapter_mapping = {ch.id: ch.title for ch in chapters} + + # 构建角色ID到名称的映射 + char_result = await db.execute( + select(Character).where(Character.project_id == project_id) + ) + characters = char_result.scalars().all() + char_mapping = {char.id: char.name for char in characters} + + result = await db.execute( + select(StoryMemory) + .where(StoryMemory.project_id == project_id) + .order_by(StoryMemory.story_timeline, StoryMemory.chapter_position) + ) + memories = result.scalars().all() + + exported = [] + for mem in memories: + # 将角色ID列表转换为名称列表 + related_char_names = None + if mem.related_characters: + related_char_names = [ + char_mapping.get(char_id, char_id) + for char_id in mem.related_characters + ] + + exported.append(StoryMemoryExportData( + chapter_title=chapter_mapping.get(mem.chapter_id) if mem.chapter_id else None, + memory_type=mem.memory_type, + title=mem.title, + content=mem.content, + full_context=mem.full_context, + related_characters=related_char_names, + related_locations=mem.related_locations, + tags=mem.tags, + importance_score=mem.importance_score or 0.5, + story_timeline=mem.story_timeline, + chapter_position=mem.chapter_position or 0, + text_length=mem.text_length or 0, + is_foreshadow=mem.is_foreshadow or 0, + foreshadow_strength=mem.foreshadow_strength, + created_at=mem.created_at.isoformat() if mem.created_at else None + )) + + return exported + + @staticmethod + async def _export_plot_analysis(project_id: str, db: AsyncSession) -> List[PlotAnalysisExportData]: + """导出剧情分析""" + # 构建章节ID到标题的映射 + chapter_result = await db.execute( + select(Chapter).where(Chapter.project_id == project_id) + ) + chapters = chapter_result.scalars().all() + chapter_mapping = {ch.id: ch.title for ch in chapters} + + result = await db.execute( + select(PlotAnalysis) + .where(PlotAnalysis.project_id == project_id) + ) + analyses = result.scalars().all() + + exported = [] + for analysis in analyses: + chapter_title = chapter_mapping.get(analysis.chapter_id) + if not chapter_title: + continue # 跳过没有关联章节的分析 + + exported.append(PlotAnalysisExportData( + chapter_title=chapter_title, + plot_stage=analysis.plot_stage, + conflict_level=analysis.conflict_level, + conflict_types=analysis.conflict_types, + emotional_tone=analysis.emotional_tone, + emotional_intensity=analysis.emotional_intensity, + emotional_curve=analysis.emotional_curve, + hooks=analysis.hooks, + hooks_count=analysis.hooks_count or 0, + hooks_avg_strength=analysis.hooks_avg_strength, + foreshadows=analysis.foreshadows, + foreshadows_planted=analysis.foreshadows_planted or 0, + foreshadows_resolved=analysis.foreshadows_resolved or 0, + plot_points=analysis.plot_points, + plot_points_count=analysis.plot_points_count or 0, + character_states=analysis.character_states, + scenes=analysis.scenes, + pacing=analysis.pacing, + overall_quality_score=analysis.overall_quality_score, + pacing_score=analysis.pacing_score, + engagement_score=analysis.engagement_score, + coherence_score=analysis.coherence_score, + analysis_report=analysis.analysis_report, + suggestions=analysis.suggestions, + word_count=analysis.word_count, + dialogue_ratio=analysis.dialogue_ratio, + description_ratio=analysis.description_ratio, + created_at=analysis.created_at.isoformat() if analysis.created_at else None + )) + + return exported + + @staticmethod + async def _export_project_default_style(project_id: str, db: AsyncSession) -> Optional[ProjectDefaultStyleExportData]: + """导出项目默认风格""" + result = await db.execute( + select(ProjectDefaultStyle, WritingStyle) + .join(WritingStyle, ProjectDefaultStyle.style_id == WritingStyle.id) + .where(ProjectDefaultStyle.project_id == project_id) + ) + row = result.first() + + if row: + _, style = row + return ProjectDefaultStyleExportData(style_name=style.name) + + return None + @staticmethod def validate_import_data(data: Dict) -> ImportValidationResult: """ @@ -413,8 +638,8 @@ class ImportExportService: version = data.get("version", "") if not version: errors.append("缺少版本信息") - elif version != ImportExportService.SUPPORTED_VERSION: - warnings.append(f"版本不匹配: 导入文件版本为 {version}, 当前支持版本为 {ImportExportService.SUPPORTED_VERSION}") + elif version not in ImportExportService.SUPPORTED_VERSIONS: + warnings.append(f"版本不匹配: 导入文件版本为 {version}, 当前支持版本为 {', '.join(ImportExportService.SUPPORTED_VERSIONS)}") # 检查必需字段 if "project" not in data: @@ -424,7 +649,7 @@ class ImportExportService: if not project.get("title"): errors.append("项目标题不能为空") - # 统计数据 + # 统计数据(包含新增字段) statistics = { "chapters": len(data.get("chapters", [])), "characters": len(data.get("characters", [])), @@ -433,7 +658,12 @@ class ImportExportService: "organizations": len(data.get("organizations", [])), "organization_members": len(data.get("organization_members", [])), "writing_styles": len(data.get("writing_styles", [])), - "generation_history": len(data.get("generation_history", [])) + "generation_history": len(data.get("generation_history", [])), + "careers": len(data.get("careers", [])), + "character_careers": len(data.get("character_careers", [])), + "story_memories": len(data.get("story_memories", [])), + "plot_analysis": len(data.get("plot_analysis", [])), + "has_default_style": data.get("project_default_style") is not None } # 检查数据完整性 @@ -565,6 +795,58 @@ class ImportExportService: statistics["writing_styles"] = styles_count logger.info(f"导入写作风格数: {styles_count}") + # 导入职业系统 + career_mapping = await ImportExportService._import_careers( + new_project.id, data.get("careers", []), db + ) + statistics["careers"] = len(career_mapping) + logger.info(f"导入职业数: {len(career_mapping)}") + + # 导入角色职业关联 + char_careers_count = await ImportExportService._import_character_careers( + data.get("character_careers", []), char_mapping, career_mapping, db + ) + statistics["character_careers"] = char_careers_count + logger.info(f"导入角色职业关联数: {char_careers_count}") + + # 导入故事记忆 + # 需要先构建章节标题到ID的映射 + chapter_title_to_id = {} + for ch_data in data.get("chapters", []): + title = ch_data.get("title") + if title: + # 查询刚导入的章节 + ch_result = await db.execute( + select(Chapter).where( + Chapter.project_id == new_project.id, + Chapter.title == title + ) + ) + ch = ch_result.scalar_one_or_none() + if ch: + chapter_title_to_id[title] = ch.id + + memories_count = await ImportExportService._import_story_memories( + new_project.id, data.get("story_memories", []), chapter_title_to_id, char_mapping, db + ) + statistics["story_memories"] = memories_count + logger.info(f"导入故事记忆数: {memories_count}") + + # 导入剧情分析 + plot_analysis_count = await ImportExportService._import_plot_analysis( + new_project.id, data.get("plot_analysis", []), chapter_title_to_id, db + ) + statistics["plot_analysis"] = plot_analysis_count + logger.info(f"导入剧情分析数: {plot_analysis_count}") + + # 导入项目默认风格 + default_style_imported = await ImportExportService._import_project_default_style( + new_project.id, data.get("project_default_style"), db + ) + statistics["project_default_style"] = 1 if default_style_imported else 0 + if default_style_imported: + logger.info("导入项目默认风格成功") + # 提交事务 await db.commit() @@ -842,6 +1124,251 @@ class ImportExportService: return count + @staticmethod + async def _import_careers( + project_id: str, + careers_data: List[Dict], + db: AsyncSession + ) -> Dict[str, str]: + """导入职业,返回名称到ID的映射""" + career_mapping = {} + + for career_data in careers_data: + career = Career( + project_id=project_id, + name=career_data.get("name"), + type=career_data.get("type", "main"), + description=career_data.get("description"), + category=career_data.get("category"), + stages=career_data.get("stages", "[]"), + max_stage=career_data.get("max_stage", 10), + requirements=career_data.get("requirements"), + special_abilities=career_data.get("special_abilities"), + worldview_rules=career_data.get("worldview_rules"), + attribute_bonuses=career_data.get("attribute_bonuses"), + source=career_data.get("source", "ai") + ) + db.add(career) + await db.flush() + career_mapping[career_data.get("name")] = career.id + + return career_mapping + + @staticmethod + async def _import_character_careers( + character_careers_data: List[Dict], + char_mapping: Dict[str, str], + career_mapping: Dict[str, str], + db: AsyncSession + ) -> int: + """导入角色职业关联""" + count = 0 + for cc_data in character_careers_data: + char_name = cc_data.get("character_name") + career_name = cc_data.get("career_name") + + char_id = char_mapping.get(char_name) + career_id = career_mapping.get(career_name) + + if char_id and career_id: + # 检查是否已存在 + existing = await db.execute( + select(CharacterCareer).where( + CharacterCareer.character_id == char_id, + CharacterCareer.career_id == career_id + ) + ) + if existing.scalar_one_or_none(): + continue + + char_career = CharacterCareer( + character_id=char_id, + career_id=career_id, + career_type=cc_data.get("career_type", "main"), + current_stage=cc_data.get("current_stage", 1), + stage_progress=cc_data.get("stage_progress", 0), + started_at=cc_data.get("started_at"), + reached_current_stage_at=cc_data.get("reached_current_stage_at"), + notes=cc_data.get("notes") + ) + db.add(char_career) + count += 1 + + # 同时更新角色的主职业信息 + if cc_data.get("career_type") == "main": + char_result = await db.execute( + select(Character).where(Character.id == char_id) + ) + char = char_result.scalar_one_or_none() + if char: + char.main_career_id = career_id + char.main_career_stage = cc_data.get("current_stage", 1) + + return count + + @staticmethod + async def _import_story_memories( + project_id: str, + memories_data: List[Dict], + chapter_mapping: Dict[str, str], + char_mapping: Dict[str, str], + db: AsyncSession + ) -> int: + """导入故事记忆""" + count = 0 + for mem_data in memories_data: + # 将章节标题转换为ID + chapter_id = None + chapter_title = mem_data.get("chapter_title") + if chapter_title and chapter_title in chapter_mapping: + chapter_id = chapter_mapping[chapter_title] + + # 将角色名称列表转换为ID列表 + related_char_ids = None + related_char_names = mem_data.get("related_characters") + if related_char_names: + related_char_ids = [ + char_mapping.get(name) + for name in related_char_names + if char_mapping.get(name) + ] + + memory = StoryMemory( + project_id=project_id, + chapter_id=chapter_id, + memory_type=mem_data.get("memory_type"), + title=mem_data.get("title"), + content=mem_data.get("content"), + full_context=mem_data.get("full_context"), + related_characters=related_char_ids, + related_locations=mem_data.get("related_locations"), + tags=mem_data.get("tags"), + importance_score=mem_data.get("importance_score", 0.5), + story_timeline=mem_data.get("story_timeline", 0), + chapter_position=mem_data.get("chapter_position", 0), + text_length=mem_data.get("text_length", 0), + is_foreshadow=mem_data.get("is_foreshadow", 0), + foreshadow_strength=mem_data.get("foreshadow_strength") + ) + db.add(memory) + count += 1 + + return count + + @staticmethod + async def _import_plot_analysis( + project_id: str, + plot_data: List[Dict], + chapter_mapping: Dict[str, str], + db: AsyncSession + ) -> int: + """导入剧情分析""" + count = 0 + for analysis_data in plot_data: + chapter_title = analysis_data.get("chapter_title") + chapter_id = chapter_mapping.get(chapter_title) + + if not chapter_id: + continue # 跳过找不到章节的分析 + + # 检查是否已存在该章节的分析 + existing = await db.execute( + select(PlotAnalysis).where(PlotAnalysis.chapter_id == chapter_id) + ) + if existing.scalar_one_or_none(): + continue + + analysis = PlotAnalysis( + project_id=project_id, + chapter_id=chapter_id, + plot_stage=analysis_data.get("plot_stage"), + conflict_level=analysis_data.get("conflict_level"), + conflict_types=analysis_data.get("conflict_types"), + emotional_tone=analysis_data.get("emotional_tone"), + emotional_intensity=analysis_data.get("emotional_intensity"), + emotional_curve=analysis_data.get("emotional_curve"), + hooks=analysis_data.get("hooks"), + hooks_count=analysis_data.get("hooks_count", 0), + hooks_avg_strength=analysis_data.get("hooks_avg_strength"), + foreshadows=analysis_data.get("foreshadows"), + foreshadows_planted=analysis_data.get("foreshadows_planted", 0), + foreshadows_resolved=analysis_data.get("foreshadows_resolved", 0), + plot_points=analysis_data.get("plot_points"), + plot_points_count=analysis_data.get("plot_points_count", 0), + character_states=analysis_data.get("character_states"), + scenes=analysis_data.get("scenes"), + pacing=analysis_data.get("pacing"), + overall_quality_score=analysis_data.get("overall_quality_score"), + pacing_score=analysis_data.get("pacing_score"), + engagement_score=analysis_data.get("engagement_score"), + coherence_score=analysis_data.get("coherence_score"), + analysis_report=analysis_data.get("analysis_report"), + suggestions=analysis_data.get("suggestions"), + word_count=analysis_data.get("word_count"), + dialogue_ratio=analysis_data.get("dialogue_ratio"), + description_ratio=analysis_data.get("description_ratio") + ) + db.add(analysis) + count += 1 + + return count + + @staticmethod + async def _import_project_default_style( + project_id: str, + default_style_data: Optional[Dict], + db: AsyncSession + ) -> bool: + """导入项目默认风格""" + if not default_style_data: + return False + + style_name = default_style_data.get("style_name") + if not style_name: + return False + + # 获取项目所属用户 + project_result = await db.execute( + select(Project).where(Project.id == project_id) + ) + project = project_result.scalar_one_or_none() + if not project: + return False + + # 查找对应的风格(优先查找用户自定义风格,然后是全局预设风格) + # 先查用户自定义风格 + style_result = await db.execute( + select(WritingStyle).where( + WritingStyle.user_id == project.user_id, + WritingStyle.name == style_name + ) + ) + style = style_result.scalar_one_or_none() + + # 如果用户自定义风格不存在,查找全局预设风格 + if not style: + style_result = await db.execute( + select(WritingStyle).where( + WritingStyle.user_id.is_(None), + WritingStyle.name == style_name + ) + ) + style = style_result.scalar_one_or_none() + + if not style: + logger.warning(f"导入项目默认风格时未找到风格: {style_name}") + return False + + # 创建项目默认风格关联 + default_style = ProjectDefaultStyle( + project_id=project_id, + style_id=style.id + ) + db.add(default_style) + + logger.info(f"项目默认风格导入成功: {style_name}, style_id={style.id}") + return True + @staticmethod async def export_characters( character_ids: List[str], @@ -919,7 +1446,7 @@ class ImportExportService: exported_characters.append(char_data) export_data = { - "version": ImportExportService.SUPPORTED_VERSION, + "version": ImportExportService.CURRENT_VERSION, "export_time": datetime.utcnow().isoformat(), "export_type": "characters", "count": len(exported_characters), @@ -1200,8 +1727,8 @@ class ImportExportService: version = data.get("version", "") if not version: errors.append("缺少版本信息") - elif version != ImportExportService.SUPPORTED_VERSION: - warnings.append(f"版本不匹配: 导入文件版本为 {version}, 当前支持版本为 {ImportExportService.SUPPORTED_VERSION}") + elif version not in ImportExportService.SUPPORTED_VERSIONS: + warnings.append(f"版本不匹配: 导入文件版本为 {version}, 当前支持版本为 {', '.join(ImportExportService.SUPPORTED_VERSIONS)}") # 检查导出类型 export_type = data.get("export_type", "") diff --git a/frontend/src/pages/ProjectList.tsx b/frontend/src/pages/ProjectList.tsx index bb01c69..be97776 100644 --- a/frontend/src/pages/ProjectList.tsx +++ b/frontend/src/pages/ProjectList.tsx @@ -16,6 +16,26 @@ import PromptTemplates from './PromptTemplates'; const { Title, Text, Paragraph } = Typography; +/** + * 格式化字数显示 + * @param count 字数 + * @returns 格式化后的字符串,如 "1.2K", "3.5W", "1.2M" + */ +const formatWordCount = (count: number): string => { + if (count < 1000) { + return count.toString(); + } else if (count < 10000) { + // 1K - 9.9K + return (count / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; + } else if (count < 1000000) { + // 1W - 99.9W (万) + return (count / 10000).toFixed(1).replace(/\.0$/, '') + 'W'; + } else { + // 1M+ (百万) + return (count / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; + } +}; + export default function ProjectList() { const navigate = useNavigate(); const { projects, loading } = useStore(); @@ -33,7 +53,10 @@ export default function ProjectList() { const [selectedProjectIds, setSelectedProjectIds] = useState([]); const [exportOptions, setExportOptions] = useState({ includeWritingStyles: true, - includeGenerationHistory: true, + includeGenerationHistory: false, + includeCareers: true, + includeMemories: false, + includePlotAnalysis: false, }); const { refreshProjects, deleteProject } = useProjectSync(); @@ -250,7 +273,10 @@ export default function ProjectList() { const project = projects.find(p => p.id === projectId); await projectApi.exportProjectData(projectId, { include_generation_history: exportOptions.includeGenerationHistory, - include_writing_styles: exportOptions.includeWritingStyles + include_writing_styles: exportOptions.includeWritingStyles, + include_careers: exportOptions.includeCareers, + include_memories: exportOptions.includeMemories, + include_plot_analysis: exportOptions.includePlotAnalysis }); message.success(`项目 "${project?.title}" 导出成功`); } else { @@ -260,7 +286,10 @@ export default function ProjectList() { try { await projectApi.exportProjectData(projectId, { include_generation_history: exportOptions.includeGenerationHistory, - include_writing_styles: exportOptions.includeWritingStyles + include_writing_styles: exportOptions.includeWritingStyles, + include_careers: exportOptions.includeCareers, + include_memories: exportOptions.includeMemories, + include_plot_analysis: exportOptions.includePlotAnalysis }); successCount++; await new Promise(resolve => setTimeout(resolve, 500)); @@ -696,7 +725,7 @@ export default function ProjectList() { {item.label} - {item.value > 10000 ? (item.value / 10000).toFixed(1) + 'w' : item.value} + {item.label === '总字数' ? formatWordCount(item.value) : item.value} {item.unit && {item.unit}} @@ -763,10 +792,10 @@ export default function ProjectList() {
@@ -988,9 +1017,7 @@ export default function ProjectList() { lineHeight: 1.2, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial' }}> - {project.current_words >= 1000 - ? (project.current_words / 1000).toFixed(1) + 'K' - : (project.current_words || 0)} + {formatWordCount(project.current_words || 0)}
- {(project.target_words || 0) >= 1000 - ? ((project.target_words || 0) / 1000).toFixed(1) + 'K' - : (project.target_words || 0)} + {formatWordCount(project.target_words || 0)}
- + 数据统计: + {validationResult.statistics.chapters > 0 && 章节: {validationResult.statistics.chapters}} {validationResult.statistics.characters > 0 && 角色: {validationResult.statistics.characters}} + {validationResult.statistics.outlines > 0 && 大纲: {validationResult.statistics.outlines}} + {validationResult.statistics.relationships > 0 && 关系: {validationResult.statistics.relationships}} + {validationResult.statistics.organizations > 0 && 组织: {validationResult.statistics.organizations}} + {validationResult.statistics.careers > 0 && 职业: {validationResult.statistics.careers}} + {validationResult.statistics.character_careers > 0 && 职业关联: {validationResult.statistics.character_careers}} + {validationResult.statistics.writing_styles > 0 && 写作风格: {validationResult.statistics.writing_styles}} + {validationResult.statistics.story_memories > 0 && 故事记忆: {validationResult.statistics.story_memories}} + {validationResult.statistics.plot_analysis > 0 && 剧情分析: {validationResult.statistics.plot_analysis}} + {validationResult.statistics.generation_history > 0 && 生成历史: {validationResult.statistics.generation_history}} + {validationResult.statistics.has_default_style && 含默认风格}
)} + {validationResult.warnings?.length > 0 && ( +
+ 提示: +
    + {validationResult.warnings.map((w: string, i: number) =>
  • {w}
  • )} +
+
+ )} {validationResult.errors?.length > 0 && (
错误: @@ -1155,12 +1199,21 @@ export default function ProjectList() { > - + 导出选项 - - setExportOptions(prev => ({...prev, includeWritingStyles: e.target.checked}))}>包含写作风格 - setExportOptions(prev => ({...prev, includeGenerationHistory: e.target.checked}))}>包含生成历史 - +
+ setExportOptions(prev => ({...prev, includeWritingStyles: e.target.checked}))}>写作风格 + setExportOptions(prev => ({...prev, includeCareers: e.target.checked}))}>职业系统 + + setExportOptions(prev => ({...prev, includeGenerationHistory: e.target.checked}))}>生成历史 + + + setExportOptions(prev => ({...prev, includeMemories: e.target.checked}))}>故事记忆 + + + setExportOptions(prev => ({...prev, includePlotAnalysis: e.target.checked}))}>剧情分析 + +
@@ -1194,7 +1247,7 @@ export default function ProjectList() {
{p.title}
-
{p.current_words || 0} 字 · {getStatusTag(getDisplayStatus(p.status, getProgress(p.current_words || 0, p.target_words || 0)))}
+
{formatWordCount(p.current_words || 0)} 字 · {getStatusTag(getDisplayStatus(p.status, getProgress(p.current_words || 0, p.target_words || 0)))}
))}