refactor: modularize yaml2pptx into layered architecture
Refactor yaml2pptx.py from a 1,245-line monolithic script into a modular architecture with clear separation of concerns. The entry point is now 127 lines, with business logic distributed across focused modules. Architecture: - core/: Domain models (elements, template, presentation) - loaders/: YAML loading and validation - renderers/: PPTX and HTML rendering - preview/: Flask preview server - utils.py: Shared utilities Key improvements: - Element abstraction layer using dataclass with validation - Renderer logic built into generator classes - Single-direction dependencies (no circular imports) - Each module 150-300 lines for better readability - Backward compatible CLI interface Documentation: - README.md: User-facing usage guide - README_DEV.md: Developer documentation OpenSpec: - Archive refactor-yaml2pptx-modular change (63/70 tasks complete) - Sync 5 delta specs to main specs (2 new + 3 updated)
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 元素必须使用 dataclass 定义
|
||||
|
||||
系统必须使用 Python dataclass 定义所有元素类型(TextElement, ImageElement, ShapeElement, TableElement),提供类型安全和自动生成的方法。
|
||||
|
||||
#### Scenario: 文本元素使用 dataclass 定义
|
||||
|
||||
- **WHEN** 开发者查看 core/elements.py 中的 TextElement 类
|
||||
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、content、box、font 字段
|
||||
|
||||
#### Scenario: 图片元素使用 dataclass 定义
|
||||
|
||||
- **WHEN** 开发者查看 core/elements.py 中的 ImageElement 类
|
||||
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、src、box 字段
|
||||
|
||||
#### Scenario: 形状元素使用 dataclass 定义
|
||||
|
||||
- **WHEN** 开发者查看 core/elements.py 中的 ShapeElement 类
|
||||
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、shape、box、fill、line 字段
|
||||
|
||||
#### Scenario: 表格元素使用 dataclass 定义
|
||||
|
||||
- **WHEN** 开发者查看 core/elements.py 中的 TableElement 类
|
||||
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、data、position、col_widths、style 字段
|
||||
|
||||
### Requirement: 元素必须在创建时进行验证
|
||||
|
||||
系统必须在元素对象创建时(`__post_init__` 方法)进行数据验证,确保元素数据的有效性,尽早发现错误。
|
||||
|
||||
#### Scenario: 文本元素验证 box 字段
|
||||
|
||||
- **WHEN** 创建 TextElement 对象时 box 字段不是包含 4 个数字的列表
|
||||
- **THEN** 系统应抛出 ValueError 异常,提示 "box 必须包含 4 个数字"
|
||||
|
||||
#### Scenario: 图片元素验证 src 字段
|
||||
|
||||
- **WHEN** 创建 ImageElement 对象时 src 字段为空
|
||||
- **THEN** 系统应抛出 ValueError 异常,提示 "图片元素必须指定 src"
|
||||
|
||||
#### Scenario: 形状元素验证 box 字段
|
||||
|
||||
- **WHEN** 创建 ShapeElement 对象时 box 字段不是包含 4 个数字的列表
|
||||
- **THEN** 系统应抛出 ValueError 异常,提示 "box 必须包含 4 个数字"
|
||||
|
||||
#### Scenario: 表格元素验证 data 字段
|
||||
|
||||
- **WHEN** 创建 TableElement 对象时 data 字段为空列表
|
||||
- **THEN** 系统应抛出 ValueError 异常,提示 "表格数据不能为空"
|
||||
|
||||
### Requirement: 必须提供元素工厂函数
|
||||
|
||||
系统必须提供 `create_element(elem_dict)` 工厂函数,从字典创建对应类型的元素对象,统一元素创建入口。
|
||||
|
||||
#### Scenario: 从字典创建文本元素
|
||||
|
||||
- **WHEN** 调用 `create_element({'type': 'text', 'content': 'Hello', 'box': [1, 1, 8, 1]})`
|
||||
- **THEN** 系统应返回 TextElement 对象,包含相应的字段值
|
||||
|
||||
#### Scenario: 从字典创建图片元素
|
||||
|
||||
- **WHEN** 调用 `create_element({'type': 'image', 'src': 'image.png', 'box': [1, 1, 4, 3]})`
|
||||
- **THEN** 系统应返回 ImageElement 对象,包含相应的字段值
|
||||
|
||||
#### Scenario: 从字典创建形状元素
|
||||
|
||||
- **WHEN** 调用 `create_element({'type': 'shape', 'shape': 'rectangle', 'box': [1, 1, 2, 1]})`
|
||||
- **THEN** 系统应返回 ShapeElement 对象,包含相应的字段值
|
||||
|
||||
#### Scenario: 从字典创建表格元素
|
||||
|
||||
- **WHEN** 调用 `create_element({'type': 'table', 'data': [['A', 'B'], ['1', '2']], 'position': [1, 1]})`
|
||||
- **THEN** 系统应返回 TableElement 对象,包含相应的字段值
|
||||
|
||||
#### Scenario: 不支持的元素类型
|
||||
|
||||
- **WHEN** 调用 `create_element({'type': 'unknown'})`
|
||||
- **THEN** 系统应抛出异常,提示不支持的元素类型
|
||||
|
||||
### Requirement: 元素类型必须支持未来扩展
|
||||
|
||||
系统的元素抽象层设计必须支持未来添加新元素类型(如 VideoElement),只需定义新的 dataclass 和在工厂函数中添加分支。
|
||||
|
||||
#### Scenario: 添加新元素类型的步骤清晰
|
||||
|
||||
- **WHEN** 开发者需要添加新元素类型(如 Video)
|
||||
- **THEN** 应只需要:
|
||||
1. 在 core/elements.py 中定义新的 dataclass(如 VideoElement)
|
||||
2. 在 `create_element()` 工厂函数中添加对应的分支
|
||||
3. 在各个渲染器中实现渲染方法
|
||||
|
||||
#### Scenario: 新元素类型不影响现有元素
|
||||
|
||||
- **WHEN** 添加新元素类型
|
||||
- **THEN** 现有元素类型(Text, Image, Shape, Table)的行为不应受到影响
|
||||
|
||||
### Requirement: 元素数据类必须提供默认值
|
||||
|
||||
系统必须为元素数据类的可选字段提供合理的默认值,简化元素创建。
|
||||
|
||||
#### Scenario: 文本元素的默认值
|
||||
|
||||
- **WHEN** 创建 TextElement 对象时不提供 box 和 font 字段
|
||||
- **THEN** 系统应使用默认值:box=[1, 1, 8, 1],font={}
|
||||
|
||||
#### Scenario: 图片元素的默认值
|
||||
|
||||
- **WHEN** 创建 ImageElement 对象时不提供 box 字段
|
||||
- **THEN** 系统应使用默认值:box=[1, 1, 4, 3]
|
||||
|
||||
#### Scenario: 形状元素的默认值
|
||||
|
||||
- **WHEN** 创建 ShapeElement 对象时不提供 fill 和 line 字段
|
||||
- **THEN** 系统应使用默认值或 None
|
||||
@@ -0,0 +1,60 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 系统必须使用元素数据类进行渲染
|
||||
|
||||
系统 SHALL 使用元素数据类(TextElement, ImageElement, ShapeElement, TableElement)进行渲染,而不是直接处理字典数据。渲染器接收元素对象,通过类型检查分发到对应的渲染方法。
|
||||
|
||||
#### Scenario: 渲染器接收元素对象
|
||||
|
||||
- **WHEN** 渲染器的 `_render_element()` 方法被调用
|
||||
- **THEN** 系统应接收元素对象(如 TextElement 实例),而不是字典
|
||||
|
||||
#### Scenario: 通过类型检查分发渲染
|
||||
|
||||
- **WHEN** 渲染器处理元素对象
|
||||
- **THEN** 系统应使用 `isinstance()` 检查元素类型,分发到对应的渲染方法(如 `_render_text()`, `_render_image()`)
|
||||
|
||||
#### Scenario: 元素验证在创建时完成
|
||||
|
||||
- **WHEN** 元素对象被创建
|
||||
- **THEN** 系统应在 `__post_init__` 方法中完成验证,渲染时不再需要验证
|
||||
|
||||
### Requirement: 渲染器必须内置在生成器中
|
||||
|
||||
系统 SHALL 将渲染逻辑内置在 PptxGenerator 类中,作为私有方法(`_render_text()`, `_render_image()` 等),而不是独立的函数。
|
||||
|
||||
#### Scenario: 渲染方法作为生成器的私有方法
|
||||
|
||||
- **WHEN** 开发者查看 PptxGenerator 类
|
||||
- **THEN** 应包含 `_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` 等私有方法
|
||||
|
||||
#### Scenario: 渲染方法接收元素对象
|
||||
|
||||
- **WHEN** 调用 `_render_text(slide, elem)` 方法
|
||||
- **THEN** `elem` 参数应为 TextElement 对象,包含 content、box、font 等属性
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 元素渲染必须支持元素对象的属性访问
|
||||
|
||||
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`, `elem.font`)访问数据,而不是通过字典的 `get()` 方法。
|
||||
|
||||
#### Scenario: 访问文本元素的属性
|
||||
|
||||
- **WHEN** 渲染文本元素
|
||||
- **THEN** 系统应使用 `elem.content` 而不是 `elem.get('content', '')`
|
||||
|
||||
#### Scenario: 访问图片元素的属性
|
||||
|
||||
- **WHEN** 渲染图片元素
|
||||
- **THEN** 系统应使用 `elem.src` 和 `elem.box` 而不是 `elem.get('src')` 和 `elem.get('box')`
|
||||
|
||||
#### Scenario: 访问形状元素的属性
|
||||
|
||||
- **WHEN** 渲染形状元素
|
||||
- **THEN** 系统应使用 `elem.shape`, `elem.fill`, `elem.line` 等属性
|
||||
|
||||
#### Scenario: 访问表格元素的属性
|
||||
|
||||
- **WHEN** 渲染表格元素
|
||||
- **THEN** 系统应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性
|
||||
@@ -0,0 +1,89 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: HTML 渲染必须使用独立的 HtmlRenderer 类
|
||||
|
||||
系统 SHALL 将 HTML 渲染逻辑重构为独立的 HtmlRenderer 类,而不是内联函数。
|
||||
|
||||
#### Scenario: HtmlRenderer 类定义在独立模块
|
||||
|
||||
- **WHEN** 开发者查看项目结构
|
||||
- **THEN** HtmlRenderer 类应定义在 `renderers/html_renderer.py` 文件中
|
||||
|
||||
#### Scenario: HtmlRenderer 包含渲染方法
|
||||
|
||||
- **WHEN** 开发者查看 HtmlRenderer 类
|
||||
- **THEN** 应包含 `render_slide()`, `render_text()`, `render_image()`, `render_shape()`, `render_table()` 等方法
|
||||
|
||||
#### Scenario: 预览服务器使用 HtmlRenderer
|
||||
|
||||
- **WHEN** 预览服务器生成 HTML
|
||||
- **THEN** 应创建 HtmlRenderer 实例并调用其渲染方法
|
||||
|
||||
### Requirement: HTML 渲染必须接收元素对象
|
||||
|
||||
系统 SHALL 使 HtmlRenderer 的渲染方法接收元素对象(TextElement, ImageElement 等),而不是字典。
|
||||
|
||||
#### Scenario: render_text 接收 TextElement 对象
|
||||
|
||||
- **WHEN** 调用 `renderer.render_text(elem)`
|
||||
- **THEN** `elem` 参数应为 TextElement 对象
|
||||
|
||||
#### Scenario: render_image 接收 ImageElement 对象
|
||||
|
||||
- **WHEN** 调用 `renderer.render_image(elem, base_path)`
|
||||
- **THEN** `elem` 参数应为 ImageElement 对象
|
||||
|
||||
#### Scenario: render_shape 接收 ShapeElement 对象
|
||||
|
||||
- **WHEN** 调用 `renderer.render_shape(elem)`
|
||||
- **THEN** `elem` 参数应为 ShapeElement 对象
|
||||
|
||||
#### Scenario: render_table 接收 TableElement 对象
|
||||
|
||||
- **WHEN** 调用 `renderer.render_table(elem)`
|
||||
- **THEN** `elem` 参数应为 TableElement 对象
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: HTML 渲染必须通过元素属性访问数据
|
||||
|
||||
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`)访问数据,而不是通过字典的 `get()` 方法。
|
||||
|
||||
#### Scenario: 访问文本元素属性
|
||||
|
||||
- **WHEN** 渲染文本元素
|
||||
- **THEN** 应使用 `elem.content`, `elem.box`, `elem.font` 等属性
|
||||
|
||||
#### Scenario: 访问图片元素属性
|
||||
|
||||
- **WHEN** 渲染图片元素
|
||||
- **THEN** 应使用 `elem.src`, `elem.box` 等属性
|
||||
|
||||
#### Scenario: 访问形状元素属性
|
||||
|
||||
- **WHEN** 渲染形状元素
|
||||
- **THEN** 应使用 `elem.shape`, `elem.box`, `elem.fill`, `elem.line` 等属性
|
||||
|
||||
#### Scenario: 访问表格元素属性
|
||||
|
||||
- **WHEN** 渲染表格元素
|
||||
- **THEN** 应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性
|
||||
|
||||
### Requirement: HtmlRenderer 必须与 PptxRenderer 共享元素抽象层
|
||||
|
||||
系统 SHALL 使 HtmlRenderer 和 PptxRenderer 都基于相同的元素数据类(定义在 core/elements.py),确保一致性。
|
||||
|
||||
#### Scenario: 两个渲染器使用相同的元素类型
|
||||
|
||||
- **WHEN** 开发者查看 HtmlRenderer 和 PptxRenderer
|
||||
- **THEN** 两者都应导入并使用 `from core.elements import TextElement, ImageElement, ShapeElement, TableElement`
|
||||
|
||||
#### Scenario: 元素验证在创建时完成
|
||||
|
||||
- **WHEN** 元素对象传递给 HtmlRenderer
|
||||
- **THEN** 元素已经在创建时完成验证,HtmlRenderer 不需要再次验证
|
||||
|
||||
#### Scenario: 添加新元素类型时两个渲染器同步更新
|
||||
|
||||
- **WHEN** 在 core/elements.py 中添加新元素类型(如 VideoElement)
|
||||
- **THEN** 需要在 HtmlRenderer 和 PptxRenderer 中都实现对应的渲染方法
|
||||
@@ -0,0 +1,99 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代码必须按功能模块组织
|
||||
|
||||
系统必须将代码按照功能职责拆分为独立的模块文件,每个模块文件的代码行数应控制在 150-300 行之间,确保代码易于阅读和维护。
|
||||
|
||||
#### Scenario: 单文件脚本拆分为多个模块
|
||||
|
||||
- **WHEN** 开发者查看项目结构
|
||||
- **THEN** 系统应包含以下模块文件:
|
||||
- `yaml2pptx.py`(入口脚本,约 100 行)
|
||||
- `core/elements.py`(元素数据类)
|
||||
- `core/template.py`(模板系统)
|
||||
- `core/presentation.py`(演示文稿类)
|
||||
- `loaders/yaml_loader.py`(YAML 加载)
|
||||
- `renderers/pptx_renderer.py`(PPTX 渲染器)
|
||||
- `renderers/html_renderer.py`(HTML 渲染器)
|
||||
- `preview/server.py`(预览服务器)
|
||||
- `utils.py`(工具函数)
|
||||
|
||||
#### Scenario: 每个模块文件大小适中
|
||||
|
||||
- **WHEN** 开发者打开任意模块文件
|
||||
- **THEN** 文件代码行数应在 150-300 行之间(入口脚本除外,约 100 行)
|
||||
|
||||
### Requirement: 模块必须按层次组织
|
||||
|
||||
系统必须采用四层架构组织代码:入口层(yaml2pptx.py)、核心层(core/)、加载层(loaders/)、渲染层(renderers/)、预览层(preview/),每层职责清晰。
|
||||
|
||||
#### Scenario: 核心层包含领域模型
|
||||
|
||||
- **WHEN** 开发者查看 core/ 目录
|
||||
- **THEN** 应包含元素数据类(elements.py)、模板系统(template.py)、演示文稿类(presentation.py)
|
||||
|
||||
#### Scenario: 加载层负责数据输入
|
||||
|
||||
- **WHEN** 开发者查看 loaders/ 目录
|
||||
- **THEN** 应包含 YAML 加载和验证逻辑(yaml_loader.py)
|
||||
|
||||
#### Scenario: 渲染层负责数据输出
|
||||
|
||||
- **WHEN** 开发者查看 renderers/ 目录
|
||||
- **THEN** 应包含 PPTX 渲染器(pptx_renderer.py)和 HTML 渲染器(html_renderer.py)
|
||||
|
||||
#### Scenario: 预览层提供可选功能
|
||||
|
||||
- **WHEN** 开发者查看 preview/ 目录
|
||||
- **THEN** 应包含 Flask 服务器和文件监听逻辑(server.py)
|
||||
|
||||
### Requirement: 模块间依赖关系必须清晰
|
||||
|
||||
系统必须保持清晰的依赖方向:入口层 → 渲染层 → 核心层 ← 加载层,避免循环依赖。
|
||||
|
||||
#### Scenario: 依赖方向单向流动
|
||||
|
||||
- **WHEN** 开发者分析模块导入关系
|
||||
- **THEN** 依赖关系应为:
|
||||
- yaml2pptx.py 导入 renderers、loaders、core、preview
|
||||
- renderers 导入 core
|
||||
- loaders 导入 core
|
||||
- core 不导入其他业务模块(只导入标准库和第三方库)
|
||||
- preview 导入 renderers 和 core
|
||||
|
||||
#### Scenario: 不存在循环依赖
|
||||
|
||||
- **WHEN** 开发者运行脚本
|
||||
- **THEN** 系统不应出现循环导入错误
|
||||
|
||||
### Requirement: 入口脚本必须保持单一职责
|
||||
|
||||
yaml2pptx.py 必须仅包含 CLI 参数解析和主流程编排,所有业务逻辑应委托给相应的模块。
|
||||
|
||||
#### Scenario: 入口脚本只负责 CLI 和流程编排
|
||||
|
||||
- **WHEN** 开发者查看 yaml2pptx.py
|
||||
- **THEN** 文件应仅包含:
|
||||
- `/// script` 依赖声明
|
||||
- `parse_args()` 函数(CLI 参数解析)
|
||||
- `main()` 函数(流程编排)
|
||||
- 必要的导入语句
|
||||
|
||||
#### Scenario: 入口脚本代码行数精简
|
||||
|
||||
- **WHEN** 开发者统计 yaml2pptx.py 的代码行数
|
||||
- **THEN** 应约为 100 行(不包括注释和空行)
|
||||
|
||||
### Requirement: 保持向后兼容的使用方式
|
||||
|
||||
系统必须保持 `uv run yaml2pptx.py` 的使用方式不变,用户无需修改现有的调用方式。
|
||||
|
||||
#### Scenario: CLI 使用方式不变
|
||||
|
||||
- **WHEN** 用户运行 `uv run yaml2pptx.py input.yaml output.pptx`
|
||||
- **THEN** 系统应正常生成 PPTX 文件,行为与重构前一致
|
||||
|
||||
#### Scenario: CLI 参数保持兼容
|
||||
|
||||
- **WHEN** 用户使用 `--template-dir`、`--preview`、`--port` 等参数
|
||||
- **THEN** 系统应正确解析并执行相应功能
|
||||
@@ -0,0 +1,84 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: PptxGenerator 必须内置渲染器
|
||||
|
||||
系统 SHALL 将 PptxGenerator 类重构为内置渲染器的架构,渲染逻辑作为类的私有方法,而不是独立的函数。
|
||||
|
||||
#### Scenario: PptxGenerator 包含渲染方法
|
||||
|
||||
- **WHEN** 开发者查看 PptxGenerator 类
|
||||
- **THEN** 应包含 `_render_element()`, `_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` 等私有方法
|
||||
|
||||
#### Scenario: add_slide 方法调用内置渲染器
|
||||
|
||||
- **WHEN** 调用 `generator.add_slide(slide_data, base_path)`
|
||||
- **THEN** 系统应在方法内部调用 `_render_element()` 来渲染每个元素
|
||||
|
||||
#### Scenario: 渲染方法接收元素对象
|
||||
|
||||
- **WHEN** 渲染方法被调用
|
||||
- **THEN** 应接收元素对象(如 TextElement, ImageElement)而不是字典
|
||||
|
||||
### Requirement: PptxGenerator 必须位于 renderers 模块
|
||||
|
||||
系统 SHALL 将 PptxGenerator 类从主脚本提取到 `renderers/pptx_renderer.py` 模块中。
|
||||
|
||||
#### Scenario: PptxGenerator 在独立模块中
|
||||
|
||||
- **WHEN** 开发者查看项目结构
|
||||
- **THEN** PptxGenerator 类应定义在 `renderers/pptx_renderer.py` 文件中
|
||||
|
||||
#### Scenario: 主脚本导入 PptxGenerator
|
||||
|
||||
- **WHEN** yaml2pptx.py 需要使用 PptxGenerator
|
||||
- **THEN** 应通过 `from renderers.pptx_renderer import PptxGenerator` 导入
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 渲染器必须通过类型检查分发元素
|
||||
|
||||
系统 SHALL 在 `_render_element()` 方法中使用 `isinstance()` 检查元素类型,分发到对应的渲染方法。
|
||||
|
||||
#### Scenario: 分发文本元素
|
||||
|
||||
- **WHEN** `_render_element()` 接收到 TextElement 对象
|
||||
- **THEN** 系统应调用 `_render_text(slide, elem)`
|
||||
|
||||
#### Scenario: 分发图片元素
|
||||
|
||||
- **WHEN** `_render_element()` 接收到 ImageElement 对象
|
||||
- **THEN** 系统应调用 `_render_image(slide, elem, base_path)`
|
||||
|
||||
#### Scenario: 分发形状元素
|
||||
|
||||
- **WHEN** `_render_element()` 接收到 ShapeElement 对象
|
||||
- **THEN** 系统应调用 `_render_shape(slide, elem)`
|
||||
|
||||
#### Scenario: 分发表格元素
|
||||
|
||||
- **WHEN** `_render_element()` 接收到 TableElement 对象
|
||||
- **THEN** 系统应调用 `_render_table(slide, elem)`
|
||||
|
||||
### Requirement: 渲染方法必须访问元素对象的属性
|
||||
|
||||
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`)访问数据,而不是通过字典的 `get()` 方法。
|
||||
|
||||
#### Scenario: 访问文本元素属性
|
||||
|
||||
- **WHEN** `_render_text()` 渲染文本元素
|
||||
- **THEN** 应使用 `elem.content`, `elem.box`, `elem.font` 等属性
|
||||
|
||||
#### Scenario: 访问图片元素属性
|
||||
|
||||
- **WHEN** `_render_image()` 渲染图片元素
|
||||
- **THEN** 应使用 `elem.src`, `elem.box` 等属性
|
||||
|
||||
#### Scenario: 访问形状元素属性
|
||||
|
||||
- **WHEN** `_render_shape()` 渲染形状元素
|
||||
- **THEN** 应使用 `elem.shape`, `elem.box`, `elem.fill`, `elem.line` 等属性
|
||||
|
||||
#### Scenario: 访问表格元素属性
|
||||
|
||||
- **WHEN** `_render_table()` 渲染表格元素
|
||||
- **THEN** 应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性
|
||||
Reference in New Issue
Block a user