""" 文章排版样式管理模块 负责管理文档的排版样式,包括内置样式和自定义样式。 支持样式的创建、编辑、删除、导入和导出等功能。 """ import os import json from typing import Dict, Any, List, Optional, Union from dataclasses import dataclass, asdict, field from copy import deepcopy @dataclass class FontStyle: """字体样式配置""" name: str = "宋体" size: int = 12 bold: bool = False italic: bool = False color: str = "#000000" # RGB颜色值 @dataclass class ParagraphStyle: """段落样式配置""" line_spacing: float = 1.5 space_before: int = 0 # 段前间距(磅) space_after: int = 0 # 段后间距(磅) first_line_indent: float = 0.0 # 首行缩进(字符) alignment: str = "left" # left, center, right, justify keep_with_next: bool = False # 与下段同页 @dataclass class HeadingStyle: """标题样式配置""" font: FontStyle paragraph: ParagraphStyle numbering: bool = False # 是否自动编号 outline_level: int = 0 # 大纲级别 @dataclass class ListStyle: """列表样式配置""" font: FontStyle paragraph: ParagraphStyle bullet_symbol: str = "•" # 无序列表符号 number_format: str = "1." # 有序列表格式 @dataclass class SpecialStyle: """特殊元素样式配置""" font: FontStyle paragraph: ParagraphStyle background_color: str = "#F5F5F5" # 背景色 border: bool = False # 是否有边框 @dataclass class DocumentStyle: """文档排版样式配置""" # 基础信息 name: str = "默认样式" description: str = "标准文档样式" author: str = "系统" version: str = "1.0" # 页面设置 page_margin_top: float = 2.54 # 页边距(cm) page_margin_bottom: float = 2.54 page_margin_left: float = 3.17 page_margin_right: float = 3.17 # 基础字体和段落 body_font: Optional[FontStyle] = None body_paragraph: Optional[ParagraphStyle] = None # 标题样式 (1-6级) heading_styles: Optional[Dict[int, HeadingStyle]] = None # 列表样式 unordered_list: Optional[ListStyle] = None ordered_list: Optional[ListStyle] = None # 特殊元素样式 code_block: Optional[SpecialStyle] = None quote_block: Optional[SpecialStyle] = None table_style: Optional[SpecialStyle] = None def __post_init__(self): """初始化默认值""" if self.body_font is None: self.body_font = FontStyle() if self.body_paragraph is None: self.body_paragraph = ParagraphStyle() if self.heading_styles is None: self.heading_styles = self._create_default_headings() if self.unordered_list is None: self.unordered_list = ListStyle( font=FontStyle(), paragraph=ParagraphStyle(space_after=6) ) if self.ordered_list is None: self.ordered_list = ListStyle( font=FontStyle(), paragraph=ParagraphStyle(space_after=6) ) if self.code_block is None: self.code_block = SpecialStyle( font=FontStyle(name="Courier New", size=10), paragraph=ParagraphStyle(space_before=6, space_after=6), background_color="#F5F5F5" ) if self.quote_block is None: self.quote_block = SpecialStyle( font=FontStyle(italic=True), paragraph=ParagraphStyle(first_line_indent=2.0, space_before=6, space_after=6), background_color="#F9F9F9" ) if self.table_style is None: self.table_style = SpecialStyle( font=FontStyle(size=11), paragraph=ParagraphStyle(space_before=6, space_after=6), border=True ) def _create_default_headings(self) -> Dict[int, HeadingStyle]: """创建默认标题样式""" headings = {} base_sizes = [18, 16, 14, 13, 12, 11] # 各级标题字号 for level in range(1, 7): font_size = base_sizes[level - 1] if level <= len(base_sizes) else 11 headings[level] = HeadingStyle( font=FontStyle( name="微软雅黑", size=font_size, bold=True, color="#1F497D" ), paragraph=ParagraphStyle( line_spacing=1.3, space_before=12 if level <= 2 else 6, space_after=6, keep_with_next=True ), outline_level=level ) return headings class StyleManager: """排版样式管理器""" def __init__(self, styles_dir: str = "data/styles"): """ 初始化样式管理器 Args: styles_dir: 样式文件存储目录 """ self.styles_dir = styles_dir self.builtin_styles = self._create_builtin_styles() self.custom_styles = {} self._load_custom_styles() def _create_builtin_styles(self) -> Dict[str, DocumentStyle]: """创建内置样式(专业版)""" styles = {} # 1. 爆款文章风格 - 参考知乎、头条等平台 viral_style = DocumentStyle( name="爆款文章风格", description="高阅读量爆款文章风格,层次分明,吸引眼球", author="系统内置", body_font=FontStyle(name="微软雅黑", size=14, color="#333333"), body_paragraph=ParagraphStyle( line_spacing=1.8, first_line_indent=0.0, # 爆款文章不缩进 space_after=12, space_before=0 ) ) # 设置爆款文章的标题样式 if viral_style.heading_styles: # 主标题:大胆吸引眼球 viral_style.heading_styles[1].font = FontStyle(name="黑体", size=22, bold=True, color="#FF4500") viral_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.3, space_before=20, space_after=20, alignment="center" ) # 二级标题:强烈对比 viral_style.heading_styles[2].font = FontStyle(name="微软雅黑", size=18, bold=True, color="#FF6B35") viral_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.4, space_before=16, space_after=12, alignment="left" ) # 三级标题:精彩分段 viral_style.heading_styles[3].font = FontStyle(name="微软雅黑", size=16, bold=True, color="#4CAF50") viral_style.heading_styles[3].paragraph = ParagraphStyle( line_spacing=1.4, space_before=12, space_after=8 ) # 四级及以下:细分点 for level in range(4, 7): viral_style.heading_styles[level].font = FontStyle(name="微软雅黑", size=15, bold=True, color="#2196F3") viral_style.heading_styles[level].paragraph = ParagraphStyle( line_spacing=1.3, space_before=8, space_after=6 ) # 设置特殊元素样式 viral_style.quote_block = SpecialStyle( font=FontStyle(name="楷体", size=13, italic=True, color="#666666"), paragraph=ParagraphStyle( line_spacing=1.6, space_before=10, space_after=10, first_line_indent=0, alignment="center" ), background_color="#F8F9FA" ) styles["爆款文章风格"] = viral_style # 2. 微信公众号风格 - 专业的新媒体排版 wechat_style = DocumentStyle( name="微信公众号风格", description="专业的微信公众号排版,阅读体验佳", author="系统内置", body_font=FontStyle(name="微软雅黑", size=14, color="#3C4043"), body_paragraph=ParagraphStyle( line_spacing=1.75, first_line_indent=0.0, space_after=14, space_before=0 ) ) if wechat_style.heading_styles: # 公众号标题的精心设计 wechat_style.heading_styles[1].font = FontStyle(name="黑体", size=20, bold=True, color="#1AAD19") wechat_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.2, space_before=18, space_after=16, alignment="center" ) wechat_style.heading_styles[2].font = FontStyle(name="微软雅黑", size=17, bold=True, color="#576B95") wechat_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.3, space_before=14, space_after=10 ) wechat_style.heading_styles[3].font = FontStyle(name="微软雅黑", size=15, bold=True, color="#FA5151") wechat_style.heading_styles[3].paragraph = ParagraphStyle( line_spacing=1.3, space_before=10, space_after=8 ) # 微信公众号的引用样式 wechat_style.quote_block = SpecialStyle( font=FontStyle(name="微软雅黑", size=13, italic=True, color="#888888"), paragraph=ParagraphStyle( line_spacing=1.5, space_before=12, space_after=12, first_line_indent=1.0, alignment="left" ), background_color="#F7F7F7", border=True ) styles["微信公众号风格"] = wechat_style # 3. 知乎高赞回答风格 - 逻辑清晰,层次分明 zhihu_style = DocumentStyle( name="知乎高赞回答风格", description="逻辑清晰,层次分明,专业权威", author="系统内置", body_font=FontStyle(name="微软雅黑", size=15, color="#1A1A1A"), body_paragraph=ParagraphStyle( line_spacing=1.6, first_line_indent=0.0, space_after=10, space_before=0 ) ) if zhihu_style.heading_styles: # 知乎风格的标题设计 zhihu_style.heading_styles[1].font = FontStyle(name="黑体", size=20, bold=True, color="#0084FF") zhihu_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.3, space_before=16, space_after=14 ) zhihu_style.heading_styles[2].font = FontStyle(name="微软雅黑", size=17, bold=True, color="#00A6FB") zhihu_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.3, space_before=12, space_after=10 ) zhihu_style.heading_styles[3].font = FontStyle(name="微软雅黑", size=16, bold=True, color="#FF6B6B") zhihu_style.heading_styles[3].paragraph = ParagraphStyle( line_spacing=1.3, space_before=10, space_after=8 ) styles["知乎高赞回答风格"] = zhihu_style # 4. 小红书笔记风格 - 清新文艺,少女心 xiaohongshu_style = DocumentStyle( name="小红书笔记风格", description="清新文艺,适合生活方式类内容", author="系统内置", body_font=FontStyle(name="华文细黑", size=14, color="#333333"), body_paragraph=ParagraphStyle( line_spacing=1.8, first_line_indent=0.0, space_after=12, space_before=0 ) ) if xiaohongshu_style.heading_styles: xiaohongshu_style.heading_styles[1].font = FontStyle(name="华文细黑", size=18, bold=True, color="#FF69B4") xiaohongshu_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.2, space_before=15, space_after=12, alignment="center" ) xiaohongshu_style.heading_styles[2].font = FontStyle(name="微软雅黑", size=16, bold=True, color="#FF6EB4") xiaohongshu_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.3, space_before=12, space_after=8 ) xiaohongshu_style.heading_styles[3].font = FontStyle(name="微软雅黑", size=15, bold=True, color="#FFB6C1") xiaohongshu_style.heading_styles[3].paragraph = ParagraphStyle( line_spacing=1.3, space_before=8, space_after=6 ) styles["小红书笔记风格"] = xiaohongshu_style # 5. 今日头条新闻风格 - 信息量大,节奏紧凑 toutiao_style = DocumentStyle( name="今日头条新闻风格", description="信息密度高,节奏紧凑,突出重点", author="系统内置", body_font=FontStyle(name="微软雅黑", size=14, color="#222222"), body_paragraph=ParagraphStyle( line_spacing=1.5, first_line_indent=0.0, space_after=8, space_before=0 ) ) if toutiao_style.heading_styles: toutiao_style.heading_styles[1].font = FontStyle(name="黑体", size=19, bold=True, color="#D43F3A") toutiao_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.2, space_before=12, space_after=12 ) toutiao_style.heading_styles[2].font = FontStyle(name="微软雅黑", size=16, bold=True, color="#D9534F") toutiao_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.3, space_before=10, space_after=8 ) toutiao_style.heading_styles[3].font = FontStyle(name="微软雅黑", size=15, bold=True, color="#F0AD4E") toutiao_style.heading_styles[3].paragraph = ParagraphStyle( line_spacing=1.3, space_before=8, space_after=6 ) styles["今日头条新闻风格"] = toutiao_style # 6. B站UP主视频脚本风格 - 轻松活泼,年轻化 bilibili_style = DocumentStyle( name="B站UP主视频脚本风格", description="轻松活泼,适合年轻受众,有趣有料", author="系统内置", body_font=FontStyle(name="微软雅黑", size=14, color="#222222"), body_paragraph=ParagraphStyle( line_spacing=1.6, first_line_indent=0.0, space_after=10, space_before=0 ) ) if bilibili_style.heading_styles: bilibili_style.heading_styles[1].font = FontStyle(name="黑体", size=18, bold=True, color="#00A1D6") bilibili_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.3, space_before=14, space_after=12 ) bilibili_style.heading_styles[2].font = FontStyle(name="微软雅黑", size=16, bold=True, color="#FB7299") bilibili_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.3, space_before=10, space_after=8 ) bilibili_style.heading_styles[3].font = FontStyle(name="微软雅黑", size=15, bold=True, color="#9C88FF") bilibili_style.heading_styles[3].paragraph = ParagraphStyle( line_spacing=1.3, space_before=8, space_after=6 ) styles["B站UP主视频脚本风格"] = bilibili_style # 7. 企业微信群通知风格 - 正式严肃 enterprise_style = DocumentStyle( name="企业微信群通知风格", description="正式严肃,信息传达清晰,商务风格", author="系统内置", body_font=FontStyle(name="宋体", size=14, color="#2C2C2C"), body_paragraph=ParagraphStyle( line_spacing=1.5, first_line_indent=2.0, space_after=10, space_before=0 ) ) if enterprise_style.heading_styles: enterprise_style.heading_styles[1].font = FontStyle(name="黑体", size=18, bold=True, color="#1F4E79") enterprise_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.2, space_before=16, space_after=12, alignment="center" ) enterprise_style.heading_styles[2].font = FontStyle(name="微软雅黑", size=16, bold=True, color="#2F5597") enterprise_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.3, space_before=12, space_after=8 ) styles["企业微信群通知风格"] = enterprise_style # 8. 情感鸡汤文风格 - 温暖治愈 emotional_style = DocumentStyle( name="情感鸡汤文风格", description="温暖治愈,情感丰富,適合心灵鸡汤类内容", author="系统内置", body_font=FontStyle(name="楷体", size=14, color="#444444"), body_paragraph=ParagraphStyle( line_spacing=1.8, first_line_indent=1.5, space_after=12, space_before=0 ) ) if emotional_style.heading_styles: emotional_style.heading_styles[1].font = FontStyle(name="华文行楷", size=18, bold=True, color="#E91E63") emotional_style.heading_styles[1].paragraph = ParagraphStyle( line_spacing=1.2, space_before=16, space_after=14, alignment="center" ) emotional_style.heading_styles[2].font = FontStyle(name="楷体", size=16, bold=True, color="#FF5722") emotional_style.heading_styles[2].paragraph = ParagraphStyle( line_spacing=1.3, space_before=12, space_after=10 ) # 情感鸡汤文的引用样式 emotional_style.quote_block = SpecialStyle( font=FontStyle(name="华文行楷", size=13, italic=True, color="#795548"), paragraph=ParagraphStyle( line_spacing=1.6, space_before=10, space_after=10, alignment="center" ), background_color="#FFF3E0" ) styles["情感鸡汤文风格"] = emotional_style return styles def _load_custom_styles(self) -> None: """加载自定义样式""" if not os.path.exists(self.styles_dir): os.makedirs(self.styles_dir, exist_ok=True) return try: for filename in os.listdir(self.styles_dir): if filename.endswith('.json'): filepath = os.path.join(self.styles_dir, filename) style = self.load_style_from_file(filepath) if style: self.custom_styles[style.name] = style except Exception as e: print(f"加载自定义样式失败: {e}") def get_all_styles(self) -> Dict[str, DocumentStyle]: """获取所有样式(内置+自定义)""" all_styles = deepcopy(self.builtin_styles) all_styles.update(self.custom_styles) return all_styles def get_style_names(self) -> List[str]: """获取所有样式名称""" return list(self.get_all_styles().keys()) def get_style(self, name: str) -> Optional[DocumentStyle]: """ 获取指定样式 Args: name: 样式名称 Returns: DocumentStyle: 样式对象,如果不存在返回None """ if name in self.builtin_styles: return deepcopy(self.builtin_styles[name]) elif name in self.custom_styles: return deepcopy(self.custom_styles[name]) return None def create_custom_style(self, style: DocumentStyle) -> bool: """ 创建自定义样式 Args: style: 样式对象 Returns: bool: 是否创建成功 """ try: print(f"尝试创建自定义样式: {style.name}") # 调试信息 if style.name in self.builtin_styles: print(f"错误: {style.name} 是内置样式,不能覆盖") return False # 不能覆盖内置样式 # 检查样式对象的完整性 if not style.body_font: print("错误: 样式缺少body_font") return False if not style.body_paragraph: print("错误: 样式缺少body_paragraph") return False print("样式对象检查通过") # 保存到内存 self.custom_styles[style.name] = style print(f"样式已保存到内存: {style.name}") # 保存到文件 file_result = self.save_style_to_file(style) print(f"文件保存结果: {file_result}") if not file_result: # 如果文件保存失败,从内存中移除 del self.custom_styles[style.name] print("文件保存失败,已从内存中移除") return False print(f"样式 '{style.name}' 创建成功") return True except Exception as e: print(f"创建自定义样式异常: {e}") import traceback traceback.print_exc() return False def update_custom_style(self, style: DocumentStyle) -> bool: """ 更新自定义样式 Args: style: 样式对象 Returns: bool: 是否更新成功 """ try: if style.name in self.builtin_styles: return False # 不能修改内置样式 self.custom_styles[style.name] = style self.save_style_to_file(style) return True except Exception as e: print(f"更新自定义样式失败: {e}") return False def delete_custom_style(self, name: str) -> bool: """ 删除自定义样式 Args: name: 样式名称 Returns: bool: 是否删除成功 """ try: if name in self.builtin_styles: return False # 不能删除内置样式 if name in self.custom_styles: del self.custom_styles[name] # 删除文件 filepath = os.path.join(self.styles_dir, f"{name}.json") if os.path.exists(filepath): os.remove(filepath) return True return False except Exception as e: print(f"删除自定义样式失败: {e}") return False def save_style_to_file(self, style: DocumentStyle, filepath: Optional[str] = None) -> bool: """ 保存样式到文件 Args: style: 样式对象 filepath: 文件路径,如果为None则使用默认路径 Returns: bool: 是否保存成功 """ try: if filepath is None: os.makedirs(self.styles_dir, exist_ok=True) filepath = os.path.join(self.styles_dir, f"{style.name}.json") print(f"保存样式到文件: {filepath}") # 调试信息 # 检查目录权限 if not os.access(self.styles_dir, os.W_OK): print(f"错误: 没有写入权限 - {self.styles_dir}") return False # 尝试序列化样式对象 style_dict = asdict(style) print("样式对象序列化成功") # 尝试写入文件 with open(filepath, 'w', encoding='utf-8') as f: json.dump(style_dict, f, ensure_ascii=False, indent=2) print(f"样式文件保存成功: {filepath}") # 验证文件是否存在且可读 if os.path.exists(filepath) and os.path.getsize(filepath) > 0: print("文件存在性验证通过") return True else: print("错误: 文件保存后验证失败") return False except Exception as e: print(f"保存样式文件异常: {e}") import traceback traceback.print_exc() return False def load_style_from_file(self, filepath: str) -> Optional[DocumentStyle]: """ 从文件加载样式 Args: filepath: 文件路径 Returns: DocumentStyle: 样式对象,加载失败返回None """ try: with open(filepath, 'r', encoding='utf-8') as f: style_dict = json.load(f) # 递归转换字典为dataclass style = self._dict_to_style(style_dict) return style except Exception as e: print(f"加载样式文件失败: {e}") return None def _dict_to_style(self, data: Dict[str, Any]) -> DocumentStyle: """将字典转换为DocumentStyle对象""" # 转换嵌套的字体样式 if 'body_font' in data and data['body_font']: data['body_font'] = FontStyle(**data['body_font']) # 转换段落样式 if 'body_paragraph' in data and data['body_paragraph']: data['body_paragraph'] = ParagraphStyle(**data['body_paragraph']) # 转换标题样式 if 'heading_styles' in data and data['heading_styles']: heading_styles = {} for level, heading_data in data['heading_styles'].items(): if isinstance(heading_data, dict): font_data = heading_data.get('font', {}) para_data = heading_data.get('paragraph', {}) heading_styles[int(level)] = HeadingStyle( font=FontStyle(**font_data) if font_data else FontStyle(), paragraph=ParagraphStyle(**para_data) if para_data else ParagraphStyle(), numbering=heading_data.get('numbering', False), outline_level=heading_data.get('outline_level', int(level)) ) data['heading_styles'] = heading_styles # 转换列表样式 for list_type in ['unordered_list', 'ordered_list']: if list_type in data and data[list_type]: list_data = data[list_type] font_data = list_data.get('font', {}) para_data = list_data.get('paragraph', {}) data[list_type] = ListStyle( font=FontStyle(**font_data) if font_data else FontStyle(), paragraph=ParagraphStyle(**para_data) if para_data else ParagraphStyle(), bullet_symbol=list_data.get('bullet_symbol', '•'), number_format=list_data.get('number_format', '1.') ) # 转换特殊样式 for special_type in ['code_block', 'quote_block', 'table_style']: if special_type in data and data[special_type]: special_data = data[special_type] font_data = special_data.get('font', {}) para_data = special_data.get('paragraph', {}) data[special_type] = SpecialStyle( font=FontStyle(**font_data) if font_data else FontStyle(), paragraph=ParagraphStyle(**para_data) if para_data else ParagraphStyle(), background_color=special_data.get('background_color', '#F5F5F5'), border=special_data.get('border', False) ) return DocumentStyle(**data) def export_style(self, name: str, export_path: str) -> bool: """ 导出样式到指定路径 Args: name: 样式名称 export_path: 导出路径 Returns: bool: 是否导出成功 """ style = self.get_style(name) if style: return self.save_style_to_file(style, export_path) return False def import_style(self, import_path: str) -> Optional[str]: """ 从指定路径导入样式 Args: import_path: 导入路径 Returns: Optional[str]: 导入的样式名称,失败返回None """ style = self.load_style_from_file(import_path) if style: if self.create_custom_style(style): return style.name return None def duplicate_style(self, source_name: str, new_name: str) -> bool: """ 复制样式 Args: source_name: 源样式名称 new_name: 新样式名称 Returns: bool: 是否复制成功 """ source_style = self.get_style(source_name) if source_style and new_name not in self.get_all_styles(): source_style.name = new_name source_style.description = f"基于 {source_name} 复制" return self.create_custom_style(source_style) return False # 创建全局样式管理器实例 style_manager = StyleManager() # 兼容性函数 def get_all_styles() -> Dict[str, DocumentStyle]: """获取所有样式(兼容旧接口)""" return style_manager.get_all_styles() def get_style(name: str) -> Optional[DocumentStyle]: """获取指定样式(兼容旧接口)""" return style_manager.get_style(name)