kdtest-pw 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. kdtest_pw/__init__.py +50 -0
  2. kdtest_pw/action/__init__.py +7 -0
  3. kdtest_pw/action/base_keyword.py +292 -0
  4. kdtest_pw/action/element_plus/__init__.py +23 -0
  5. kdtest_pw/action/element_plus/el_cascader.py +263 -0
  6. kdtest_pw/action/element_plus/el_datepicker.py +324 -0
  7. kdtest_pw/action/element_plus/el_dialog.py +317 -0
  8. kdtest_pw/action/element_plus/el_form.py +443 -0
  9. kdtest_pw/action/element_plus/el_menu.py +456 -0
  10. kdtest_pw/action/element_plus/el_select.py +268 -0
  11. kdtest_pw/action/element_plus/el_table.py +442 -0
  12. kdtest_pw/action/element_plus/el_tree.py +364 -0
  13. kdtest_pw/action/element_plus/el_upload.py +313 -0
  14. kdtest_pw/action/key_retrieval.py +311 -0
  15. kdtest_pw/action/page_action.py +1129 -0
  16. kdtest_pw/api/__init__.py +6 -0
  17. kdtest_pw/api/api_keyword.py +251 -0
  18. kdtest_pw/api/request_handler.py +232 -0
  19. kdtest_pw/cases/__init__.py +6 -0
  20. kdtest_pw/cases/case_collector.py +182 -0
  21. kdtest_pw/cases/case_executor.py +359 -0
  22. kdtest_pw/cases/read/__init__.py +6 -0
  23. kdtest_pw/cases/read/cell_handler.py +305 -0
  24. kdtest_pw/cases/read/excel_reader.py +223 -0
  25. kdtest_pw/cli/__init__.py +5 -0
  26. kdtest_pw/cli/run.py +318 -0
  27. kdtest_pw/common.py +106 -0
  28. kdtest_pw/core/__init__.py +7 -0
  29. kdtest_pw/core/browser_manager.py +196 -0
  30. kdtest_pw/core/config_loader.py +235 -0
  31. kdtest_pw/core/page_context.py +228 -0
  32. kdtest_pw/data/__init__.py +5 -0
  33. kdtest_pw/data/init_data.py +105 -0
  34. kdtest_pw/data/static/elementData.yaml +59 -0
  35. kdtest_pw/data/static/parameters.json +24 -0
  36. kdtest_pw/plugins/__init__.py +6 -0
  37. kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
  38. kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
  39. kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
  40. kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
  41. kdtest_pw/plugins/plugin_base.py +180 -0
  42. kdtest_pw/plugins/plugin_loader.py +260 -0
  43. kdtest_pw/product.py +5 -0
  44. kdtest_pw/reference.py +99 -0
  45. kdtest_pw/utils/__init__.py +13 -0
  46. kdtest_pw/utils/built_in_function.py +376 -0
  47. kdtest_pw/utils/decorator.py +211 -0
  48. kdtest_pw/utils/log/__init__.py +6 -0
  49. kdtest_pw/utils/log/html_report.py +336 -0
  50. kdtest_pw/utils/log/logger.py +123 -0
  51. kdtest_pw/utils/public_script.py +366 -0
  52. kdtest_pw-2.0.0.dist-info/METADATA +169 -0
  53. kdtest_pw-2.0.0.dist-info/RECORD +57 -0
  54. kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
  55. kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
  56. kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
  57. kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,196 @@
1
+ """Playwright 浏览器生命周期管理"""
2
+
3
+ from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page, Playwright
4
+ from typing import Optional, Literal, Dict, Any
5
+ from pathlib import Path
6
+
7
+ from ..reference import GSTORE, INFO
8
+
9
+ BrowserType = Literal["chromium", "firefox", "webkit"]
10
+
11
+
12
+ class BrowserManager:
13
+ """Playwright 浏览器生命周期管理器
14
+
15
+ 负责浏览器的启动、配置和关闭。
16
+ 支持 chromium、firefox、webkit 三种浏览器。
17
+ 支持 trace 录制和视频录制。
18
+ """
19
+
20
+ def __init__(self):
21
+ self._playwright: Optional[Playwright] = None
22
+ self._browser: Optional[Browser] = None
23
+ self._context: Optional[BrowserContext] = None
24
+ self._page: Optional[Page] = None
25
+ self._trace_enabled: bool = False
26
+ self._video_enabled: bool = False
27
+
28
+ def launch(
29
+ self,
30
+ browser_type: BrowserType = "chromium",
31
+ headless: bool = False,
32
+ slow_mo: int = 0,
33
+ trace: bool = False,
34
+ video: bool = False,
35
+ timeout: int = 30000,
36
+ viewport: Optional[Dict[str, int]] = None,
37
+ locale: str = "zh-CN",
38
+ timezone_id: str = "Asia/Shanghai",
39
+ **kwargs
40
+ ) -> Page:
41
+ """启动浏览器并返回页面
42
+
43
+ Args:
44
+ browser_type: 浏览器类型 (chromium, firefox, webkit)
45
+ headless: 是否无头模式
46
+ slow_mo: 操作延迟 (毫秒)
47
+ trace: 是否开启 trace 录制
48
+ video: 是否开启视频录制
49
+ timeout: 默认超时时间 (毫秒)
50
+ viewport: 视口大小 {"width": 1920, "height": 1080}
51
+ locale: 语言区域
52
+ timezone_id: 时区
53
+ **kwargs: 其他 Playwright 启动参数
54
+
55
+ Returns:
56
+ Page: Playwright 页面实例
57
+ """
58
+ INFO(f"启动浏览器: {browser_type}, headless={headless}")
59
+
60
+ self._playwright = sync_playwright().start()
61
+
62
+ # 选择浏览器类型
63
+ browser_launcher = getattr(self._playwright, browser_type)
64
+
65
+ # 浏览器启动参数
66
+ launch_options = {
67
+ 'headless': headless,
68
+ 'slow_mo': slow_mo,
69
+ }
70
+ launch_options.update(kwargs)
71
+
72
+ self._browser = browser_launcher.launch(**launch_options)
73
+
74
+ # 创建上下文参数
75
+ context_options: Dict[str, Any] = {
76
+ 'locale': locale,
77
+ 'timezone_id': timezone_id,
78
+ }
79
+
80
+ if viewport:
81
+ context_options['viewport'] = viewport
82
+ else:
83
+ context_options['viewport'] = {'width': 1920, 'height': 1080}
84
+
85
+ # 视频录制配置
86
+ if video:
87
+ self._video_enabled = True
88
+ video_dir = Path('result/video')
89
+ video_dir.mkdir(parents=True, exist_ok=True)
90
+ context_options['record_video_dir'] = str(video_dir)
91
+ context_options['record_video_size'] = {'width': 1920, 'height': 1080}
92
+ INFO("视频录制已启用")
93
+
94
+ self._context = self._browser.new_context(**context_options)
95
+
96
+ # 开启 trace 录制
97
+ if trace:
98
+ self._trace_enabled = True
99
+ self._context.tracing.start(
100
+ screenshots=True,
101
+ snapshots=True,
102
+ sources=True
103
+ )
104
+ INFO("Trace 录制已启用")
105
+
106
+ # 创建页面
107
+ self._page = self._context.new_page()
108
+
109
+ # 设置默认超时
110
+ self._page.set_default_timeout(timeout)
111
+ self._page.set_default_navigation_timeout(timeout)
112
+
113
+ # 更新全局存储
114
+ GSTORE['browser'] = self._browser
115
+ GSTORE['context'] = self._context
116
+ GSTORE['page'] = self._page
117
+
118
+ INFO("浏览器启动成功")
119
+ return self._page
120
+
121
+ def new_page(self) -> Page:
122
+ """创建新页面
123
+
124
+ Returns:
125
+ Page: 新的页面实例
126
+ """
127
+ if not self._context:
128
+ raise RuntimeError("浏览器上下文未初始化")
129
+
130
+ page = self._context.new_page()
131
+ return page
132
+
133
+ def close(self, save_trace: Optional[str] = None) -> None:
134
+ """关闭浏览器
135
+
136
+ Args:
137
+ save_trace: trace 文件保存路径 (如果开启了 trace)
138
+ """
139
+ INFO("关闭浏览器...")
140
+
141
+ # 保存 trace
142
+ if self._trace_enabled and self._context and save_trace:
143
+ trace_path = Path(save_trace)
144
+ trace_path.parent.mkdir(parents=True, exist_ok=True)
145
+ self._context.tracing.stop(path=str(trace_path))
146
+ INFO(f"Trace 已保存: {trace_path}")
147
+
148
+ # 关闭上下文
149
+ if self._context:
150
+ self._context.close()
151
+ self._context = None
152
+
153
+ # 关闭浏览器
154
+ if self._browser:
155
+ self._browser.close()
156
+ self._browser = None
157
+
158
+ # 停止 Playwright
159
+ if self._playwright:
160
+ self._playwright.stop()
161
+ self._playwright = None
162
+
163
+ # 清理全局存储
164
+ GSTORE['browser'] = None
165
+ GSTORE['context'] = None
166
+ GSTORE['page'] = None
167
+
168
+ INFO("浏览器已关闭")
169
+
170
+ @property
171
+ def page(self) -> Optional[Page]:
172
+ """当前页面实例"""
173
+ return self._page
174
+
175
+ @property
176
+ def context(self) -> Optional[BrowserContext]:
177
+ """浏览器上下文实例"""
178
+ return self._context
179
+
180
+ @property
181
+ def browser(self) -> Optional[Browser]:
182
+ """浏览器实例"""
183
+ return self._browser
184
+
185
+ @property
186
+ def is_running(self) -> bool:
187
+ """浏览器是否在运行"""
188
+ return self._browser is not None and self._browser.is_connected()
189
+
190
+ def __enter__(self) -> 'BrowserManager':
191
+ """上下文管理器入口"""
192
+ return self
193
+
194
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
195
+ """上下文管理器出口"""
196
+ self.close()
@@ -0,0 +1,235 @@
1
+ """配置加载器"""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from ..common import DEFAULT_CONFIG
9
+ from ..reference import GSDSTORE, INFO
10
+
11
+
12
+ class ConfigLoader:
13
+ """配置加载器
14
+
15
+ 支持 JSON 和 YAML 格式的配置文件。
16
+ """
17
+
18
+ def __init__(self, config_path: Optional[str] = None):
19
+ """初始化配置加载器
20
+
21
+ Args:
22
+ config_path: 配置文件路径,支持 .json 或 .yaml/.yml
23
+ """
24
+ self._config: Dict[str, Any] = DEFAULT_CONFIG.copy()
25
+ self._config_path: Optional[Path] = None
26
+
27
+ if config_path:
28
+ self.load(config_path)
29
+
30
+ def load(self, config_path: str) -> Dict[str, Any]:
31
+ """加载配置文件
32
+
33
+ Args:
34
+ config_path: 配置文件路径
35
+
36
+ Returns:
37
+ 配置字典
38
+
39
+ Raises:
40
+ FileNotFoundError: 配置文件不存在
41
+ ValueError: 不支持的配置文件格式
42
+ """
43
+ path = Path(config_path)
44
+
45
+ if not path.exists():
46
+ raise FileNotFoundError(f"配置文件不存在: {config_path}")
47
+
48
+ self._config_path = path
49
+ suffix = path.suffix.lower()
50
+
51
+ if suffix == '.json':
52
+ config = self._load_json(path)
53
+ elif suffix in ('.yaml', '.yml'):
54
+ config = self._load_yaml(path)
55
+ else:
56
+ raise ValueError(f"不支持的配置文件格式: {suffix}")
57
+
58
+ # 合并配置
59
+ self._config.update(config)
60
+
61
+ # 处理相对路径
62
+ self._resolve_paths()
63
+
64
+ # 更新全局存储
65
+ GSDSTORE['START'] = self._config
66
+
67
+ INFO(f"配置已加载: {config_path}")
68
+ return self._config
69
+
70
+ def _load_json(self, path: Path) -> Dict[str, Any]:
71
+ """加载 JSON 配置文件"""
72
+ with open(path, 'r', encoding='utf-8') as f:
73
+ return json.load(f)
74
+
75
+ def _load_yaml(self, path: Path) -> Dict[str, Any]:
76
+ """加载 YAML 配置文件"""
77
+ try:
78
+ import yaml
79
+ with open(path, 'r', encoding='utf-8') as f:
80
+ return yaml.safe_load(f) or {}
81
+ except ImportError:
82
+ INFO("PyYAML 未安装,无法加载 YAML 配置", "WARNING")
83
+ return {}
84
+
85
+ def _resolve_paths(self) -> None:
86
+ """解析配置中的相对路径"""
87
+ if not self._config_path:
88
+ return
89
+
90
+ base_dir = self._config_path.parent
91
+
92
+ # 处理测试用例文件路径
93
+ if 'testCaseFile' in self._config:
94
+ for case_file in self._config['testCaseFile']:
95
+ if 'caseFilePath' in case_file:
96
+ case_path = Path(case_file['caseFilePath'])
97
+ if not case_path.is_absolute():
98
+ case_file['caseFilePath'] = str(base_dir / case_path)
99
+
100
+ # 存储工作路径
101
+ GSDSTORE['WORKPATH'] = {
102
+ 'config_dir': str(base_dir),
103
+ 'project_dir': str(base_dir.parent) if base_dir.name == 'data' else str(base_dir),
104
+ }
105
+
106
+ @property
107
+ def config(self) -> Dict[str, Any]:
108
+ """获取配置字典"""
109
+ return self._config.copy()
110
+
111
+ def get(self, key: str, default: Any = None) -> Any:
112
+ """获取配置项
113
+
114
+ Args:
115
+ key: 配置键名,支持点号分隔的嵌套键 (如 "browser.headless")
116
+ default: 默认值
117
+
118
+ Returns:
119
+ 配置值或默认值
120
+ """
121
+ keys = key.split('.')
122
+ value = self._config
123
+
124
+ for k in keys:
125
+ if isinstance(value, dict):
126
+ value = value.get(k)
127
+ else:
128
+ return default
129
+
130
+ if value is None:
131
+ return default
132
+
133
+ return value
134
+
135
+ def set(self, key: str, value: Any) -> None:
136
+ """设置配置项
137
+
138
+ Args:
139
+ key: 配置键名
140
+ value: 配置值
141
+ """
142
+ keys = key.split('.')
143
+
144
+ if len(keys) == 1:
145
+ self._config[key] = value
146
+ else:
147
+ # 处理嵌套键
148
+ target = self._config
149
+ for k in keys[:-1]:
150
+ if k not in target:
151
+ target[k] = {}
152
+ target = target[k]
153
+ target[keys[-1]] = value
154
+
155
+ @property
156
+ def browser(self) -> str:
157
+ """浏览器类型"""
158
+ return self._config.get('browser', 'chromium')
159
+
160
+ @property
161
+ def headless(self) -> bool:
162
+ """是否无头模式"""
163
+ return self._config.get('headless', False)
164
+
165
+ @property
166
+ def slow_mo(self) -> int:
167
+ """操作延迟"""
168
+ return self._config.get('slowMo', 0)
169
+
170
+ @property
171
+ def timeout(self) -> int:
172
+ """默认超时"""
173
+ return self._config.get('timeout', 30000)
174
+
175
+ @property
176
+ def trace(self) -> bool:
177
+ """是否开启 trace"""
178
+ return self._config.get('trace', False)
179
+
180
+ @property
181
+ def video(self) -> bool:
182
+ """是否录制视频"""
183
+ return self._config.get('video', False)
184
+
185
+ @property
186
+ def url(self) -> Optional[str]:
187
+ """起始 URL"""
188
+ return self._config.get('url')
189
+
190
+ @property
191
+ def test_case_files(self) -> List[Dict[str, Any]]:
192
+ """测试用例文件配置"""
193
+ return self._config.get('testCaseFile', [])
194
+
195
+ @property
196
+ def self_defined_parameters(self) -> Dict[str, Any]:
197
+ """自定义参数"""
198
+ return self._config.get('selfDefinedParameter', {})
199
+
200
+ @property
201
+ def retry_switch(self) -> bool:
202
+ """是否开启重试"""
203
+ return self._config.get('retrySwitch', False)
204
+
205
+ @property
206
+ def retry_count(self) -> int:
207
+ """重试次数"""
208
+ return self._config.get('retryCount', 1)
209
+
210
+ def save(self, path: Optional[str] = None) -> None:
211
+ """保存配置到文件
212
+
213
+ Args:
214
+ path: 保存路径,默认为加载时的路径
215
+ """
216
+ save_path = Path(path) if path else self._config_path
217
+
218
+ if not save_path:
219
+ raise ValueError("未指定保存路径")
220
+
221
+ suffix = save_path.suffix.lower()
222
+
223
+ if suffix == '.json':
224
+ with open(save_path, 'w', encoding='utf-8') as f:
225
+ json.dump(self._config, f, indent=4, ensure_ascii=False)
226
+ elif suffix in ('.yaml', '.yml'):
227
+ try:
228
+ import yaml
229
+ with open(save_path, 'w', encoding='utf-8') as f:
230
+ yaml.dump(self._config, f, default_flow_style=False, allow_unicode=True)
231
+ except ImportError:
232
+ INFO("PyYAML 未安装,无法保存 YAML 配置", "ERROR")
233
+ raise
234
+
235
+ INFO(f"配置已保存: {save_path}")
@@ -0,0 +1,228 @@
1
+ """页面上下文管理(多标签页支持)"""
2
+
3
+ from playwright.sync_api import Page, BrowserContext
4
+ from typing import Optional, List, Dict, Callable
5
+ from ..reference import GSTORE, PRIVATEDATA, INFO
6
+
7
+
8
+ class PageContext:
9
+ """页面上下文管理器
10
+
11
+ 管理多标签页,支持页面切换、新页面监听等功能。
12
+ """
13
+
14
+ def __init__(self, context: Optional[BrowserContext] = None):
15
+ """初始化页面上下文管理器
16
+
17
+ Args:
18
+ context: BrowserContext 实例,如果不提供则从 GSTORE 获取
19
+ """
20
+ self._context = context or GSTORE.get('context')
21
+ self._pages: List[Page] = []
22
+ self._current_index: int = 0
23
+ self._page_handlers: Dict[str, Callable] = {}
24
+
25
+ if self._context:
26
+ # 初始化已有页面
27
+ self._pages = list(self._context.pages)
28
+
29
+ # 监听新页面
30
+ self._context.on('page', self._on_new_page)
31
+
32
+ def _on_new_page(self, page: Page) -> None:
33
+ """新页面事件处理
34
+
35
+ Args:
36
+ page: 新打开的页面
37
+ """
38
+ self._pages.append(page)
39
+ INFO(f"检测到新页面: {page.url}")
40
+
41
+ # 执行注册的处理器
42
+ for handler in self._page_handlers.values():
43
+ try:
44
+ handler(page)
45
+ except Exception as e:
46
+ INFO(f"页面处理器执行失败: {e}", "ERROR")
47
+
48
+ @property
49
+ def current_page(self) -> Optional[Page]:
50
+ """获取当前活动页面"""
51
+ if not self._pages:
52
+ return GSTORE.get('page')
53
+ return self._pages[self._current_index]
54
+
55
+ @property
56
+ def page_count(self) -> int:
57
+ """获取页面数量"""
58
+ return len(self._pages)
59
+
60
+ @property
61
+ def pages(self) -> List[Page]:
62
+ """获取所有页面"""
63
+ return self._pages.copy()
64
+
65
+ def switch_to_page(self, index: int) -> Page:
66
+ """切换到指定索引的页面
67
+
68
+ Args:
69
+ index: 页面索引 (支持负数索引)
70
+
71
+ Returns:
72
+ Page: 切换后的页面
73
+
74
+ Raises:
75
+ IndexError: 索引超出范围
76
+ """
77
+ if not self._pages:
78
+ raise IndexError("没有可用的页面")
79
+
80
+ # 处理负数索引
81
+ if index < 0:
82
+ index = len(self._pages) + index
83
+
84
+ if index < 0 or index >= len(self._pages):
85
+ raise IndexError(f"页面索引超出范围: {index}")
86
+
87
+ self._current_index = index
88
+ page = self._pages[index]
89
+
90
+ # 更新全局存储
91
+ GSTORE['page'] = page
92
+ PRIVATEDATA['PAGES'] = self._pages
93
+
94
+ INFO(f"切换到页面 {index}: {page.url}")
95
+ return page
96
+
97
+ def switch_to_new_page(self, timeout: int = 30000) -> Page:
98
+ """等待并切换到新打开的页面
99
+
100
+ Args:
101
+ timeout: 等待超时 (毫秒)
102
+
103
+ Returns:
104
+ Page: 新页面
105
+ """
106
+ if not self._context:
107
+ raise RuntimeError("浏览器上下文未初始化")
108
+
109
+ with self._context.expect_page(timeout=timeout) as new_page_info:
110
+ pass
111
+
112
+ new_page = new_page_info.value
113
+ new_page.wait_for_load_state('domcontentloaded')
114
+
115
+ # 更新页面列表
116
+ if new_page not in self._pages:
117
+ self._pages.append(new_page)
118
+
119
+ self._current_index = len(self._pages) - 1
120
+ GSTORE['page'] = new_page
121
+
122
+ INFO(f"切换到新页面: {new_page.url}")
123
+ return new_page
124
+
125
+ def switch_to_page_by_url(self, url_pattern: str) -> Optional[Page]:
126
+ """根据 URL 模式切换页面
127
+
128
+ Args:
129
+ url_pattern: URL 匹配模式 (支持部分匹配)
130
+
131
+ Returns:
132
+ Page: 匹配的页面,未找到返回 None
133
+ """
134
+ for i, page in enumerate(self._pages):
135
+ if url_pattern in page.url:
136
+ return self.switch_to_page(i)
137
+
138
+ INFO(f"未找到匹配的页面: {url_pattern}", "WARNING")
139
+ return None
140
+
141
+ def switch_to_page_by_title(self, title_pattern: str) -> Optional[Page]:
142
+ """根据标题模式切换页面
143
+
144
+ Args:
145
+ title_pattern: 标题匹配模式 (支持部分匹配)
146
+
147
+ Returns:
148
+ Page: 匹配的页面,未找到返回 None
149
+ """
150
+ for i, page in enumerate(self._pages):
151
+ if title_pattern in page.title():
152
+ return self.switch_to_page(i)
153
+
154
+ INFO(f"未找到匹配的页面: {title_pattern}", "WARNING")
155
+ return None
156
+
157
+ def close_current_page(self) -> Optional[Page]:
158
+ """关闭当前页面并切换到上一个页面
159
+
160
+ Returns:
161
+ Page: 切换后的当前页面,如果没有其他页面返回 None
162
+ """
163
+ if not self._pages:
164
+ return None
165
+
166
+ current = self._pages[self._current_index]
167
+ current.close()
168
+ self._pages.pop(self._current_index)
169
+
170
+ if not self._pages:
171
+ GSTORE['page'] = None
172
+ return None
173
+
174
+ # 切换到上一个页面
175
+ if self._current_index >= len(self._pages):
176
+ self._current_index = len(self._pages) - 1
177
+
178
+ new_current = self._pages[self._current_index]
179
+ GSTORE['page'] = new_current
180
+
181
+ INFO(f"关闭页面,切换到: {new_current.url}")
182
+ return new_current
183
+
184
+ def close_page(self, index: int) -> None:
185
+ """关闭指定索引的页面
186
+
187
+ Args:
188
+ index: 页面索引
189
+ """
190
+ if index < 0 or index >= len(self._pages):
191
+ raise IndexError(f"页面索引超出范围: {index}")
192
+
193
+ page = self._pages[index]
194
+ page.close()
195
+ self._pages.pop(index)
196
+
197
+ # 调整当前索引
198
+ if self._current_index >= len(self._pages):
199
+ self._current_index = max(0, len(self._pages) - 1)
200
+
201
+ if self._pages:
202
+ GSTORE['page'] = self._pages[self._current_index]
203
+ else:
204
+ GSTORE['page'] = None
205
+
206
+ def register_page_handler(self, name: str, handler: Callable[[Page], None]) -> None:
207
+ """注册新页面处理器
208
+
209
+ Args:
210
+ name: 处理器名称
211
+ handler: 处理函数,接收 Page 参数
212
+ """
213
+ self._page_handlers[name] = handler
214
+
215
+ def unregister_page_handler(self, name: str) -> None:
216
+ """注销页面处理器
217
+
218
+ Args:
219
+ name: 处理器名称
220
+ """
221
+ self._page_handlers.pop(name, None)
222
+
223
+ def refresh(self) -> None:
224
+ """刷新页面列表(同步上下文中的页面)"""
225
+ if self._context:
226
+ self._pages = list(self._context.pages)
227
+ if self._pages and self._current_index >= len(self._pages):
228
+ self._current_index = len(self._pages) - 1
@@ -0,0 +1,5 @@
1
+ """数据模块"""
2
+
3
+ from .init_data import InitData
4
+
5
+ __all__ = ['InitData']