fix: fit main window within screen on startup

This commit is contained in:
chengma
2026-07-07 12:06:17 +08:00
parent 05a85c6d6a
commit caa04a9aa7
7 changed files with 166 additions and 5 deletions
+68 -1
View File
@@ -8,11 +8,72 @@ from .tabs.collect import CollectTab
from .tabs.generate import GenerateTab from .tabs.generate import GenerateTab
from .tabs.settings import SettingsTab from .tabs.settings import SettingsTab
from .widgets import * from .widgets import *
PREFERRED_WINDOW_SIZE = (1180, 760)
MIN_WINDOW_SIZE = (960, 640)
WINDOW_SCREEN_MARGIN = 40
def _screen_available_geometry():
app = QApplication.instance()
if app is None:
return None
screen = app.primaryScreen()
if screen is None:
return None
return screen.availableGeometry()
def _bounded_dimension(available_size, preferred_size, minimum_size, margin):
if available_size <= 0:
return preferred_size
if available_size > margin * 2:
maximum = available_size - margin * 2
else:
maximum = available_size
if maximum >= minimum_size:
return max(minimum_size, min(preferred_size, maximum))
return max(1, min(preferred_size, maximum))
def _fit_and_center_window(
window,
available_geometry=None,
preferred_size=PREFERRED_WINDOW_SIZE,
minimum_size=MIN_WINDOW_SIZE,
margin=WINDOW_SCREEN_MARGIN,
):
available = available_geometry if available_geometry is not None else _screen_available_geometry()
if available is None or available.width() <= 0 or available.height() <= 0:
window.resize(*preferred_size)
return
width = _bounded_dimension(
available.width(),
preferred_size[0],
minimum_size[0],
margin,
)
height = _bounded_dimension(
available.height(),
preferred_size[1],
minimum_size[1],
margin,
)
window.resize(width, height)
x = available.x() + max(0, (available.width() - width) // 2)
y = available.y() + max(0, (available.height() - height) // 2)
max_x = available.x() + max(0, available.width() - width)
max_y = available.y() + max(0, available.height() - height)
window.move(min(max(x, available.x()), max_x), min(max(y, available.y()), max_y))
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
"""Main application window with the fixed five-tab workflow.""" """Main application window with the fixed five-tab workflow."""
def __init__(self, db_path=None, config=None, config_path=None, ai_models_path=None): def __init__(self, db_path=None, config=None, config_path=None, ai_models_path=None):
super().__init__() super().__init__()
self._initial_window_fit_applied_after_show = False
self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config
self.config_path = ( self.config_path = (
config_path config_path
@@ -26,7 +87,7 @@ class MainWindow(QMainWindow):
or appconfig.AI_MODELS_PATH or appconfig.AI_MODELS_PATH
) )
self.setWindowTitle("蝦皮圈優化助手") self.setWindowTitle("蝦皮圈優化助手")
self.resize(1180, 760) _fit_and_center_window(self)
self.setStyleSheet(BUTTON_BASE_STYLE) self.setStyleSheet(BUTTON_BASE_STYLE)
self._settings_tab_index = TAB_TITLES.index("⑤ 设置") self._settings_tab_index = TAB_TITLES.index("⑤ 设置")
self._last_tab_index = 0 self._last_tab_index = 0
@@ -41,6 +102,12 @@ class MainWindow(QMainWindow):
self.setCentralWidget(self.tabs) self.setCentralWidget(self.tabs)
self.statusBar().showMessage("就绪") self.statusBar().showMessage("就绪")
def showEvent(self, event):
super().showEvent(event)
if not self._initial_window_fit_applied_after_show:
_fit_and_center_window(self)
self._initial_window_fit_applied_after_show = True
def _build_tab(self, title): def _build_tab(self, title):
if title == "① 导入采集": if title == "① 导入采集":
return CollectTab( return CollectTab(
+1
View File
@@ -141,6 +141,7 @@
| ID | 任务 | 依赖 | 验收要点 | 状态 | | ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| T-541 | 打包版主窗口初始位置与小屏适配 | T-523, T-537, T-540 | Bug:在 Windows 10 虚拟机/小分辨率环境运行打包后的 `dist/cmshopee/cmshopee.exe` 或 release 包时,主窗口可能贴到屏幕左上角且左边/标题栏显示不全,看不到「蝦皮圈優化助手 v<APP_VERSION>」标题,用户无法拖动窗口到中间。根因:`MainWindow.__init__()` 只调用 `self.resize(1180, 760)`,没有根据 `QApplication.primaryScreen().availableGeometry()` 限制初始尺寸、没有居中、没有保证窗口 frame/title bar 在可用屏幕范围内;PyInstaller 本身不是根因,小屏、高 DPI、任务栏占用、多显示器或 VM 分辨率更容易触发。方案:① 抽一个主窗口初始布局 helper(如 `_fit_and_center_window(window, preferred_size=(1180,760), margin=40)`),读取当前屏幕 `availableGeometry()`;② 初始宽高不超过可用区域减安全边距,保留合理最小值(如 960x640,若屏幕更小则继续适配);③ `resize()` 后按可用区域中心 `move()`,确保窗口左上角不小于可用区域左上角,标题栏完整可见;④ offscreen/headless 测试环境不依赖真实屏幕时可跳过或用 mock geometry;⑤ 不保存/恢复历史窗口位置,避免把坏坐标持久化。同步 `docs/packaging.md` 打包验收:在目标 VM/小分辨率环境启动 release exe,窗口标题栏必须完整可见且可拖动。GUI 单测覆盖小屏 mock 下尺寸被限制、窗口被居中/夹进可用区域;不改业务流程、DB、AI、Excel、CDP/Shopee、打包产物结构 | DONE |
| T-525 | 引入 ruff(lint + format)+ 可选 pre-commit | T-006 | 依据 `docs/engineering-review.md` P1。加 `ruff` 配置(lint + format),先以现状为基线不做大规模风格重排,只开启安全规则(未用 import/变量、明显错误);可选 `.pre-commit-config.yaml`;不改业务逻辑;CI(T-522)可串入 ruff 检查。数据模型渐进上 mypy 作为后续可选。当前因业务优先级调整顺延到 T-526~T-528 之后 | TODO | | T-525 | 引入 ruff(lint + format)+ 可选 pre-commit | T-006 | 依据 `docs/engineering-review.md` P1。加 `ruff` 配置(lint + format),先以现状为基线不做大规模风格重排,只开启安全规则(未用 import/变量、明显错误);可选 `.pre-commit-config.yaml`;不改业务逻辑;CI(T-522)可串入 ruff 检查。数据模型渐进上 mypy 作为后续可选。当前因业务优先级调整顺延到 T-526~T-528 之后 | TODO |
| T-537 | 品牌名「蝦皮圈優化助手」(显示层中文品牌,cmshopee 保留为代号/标识符) | T-523 | 需求:项目/软件正式定名「蝦皮圈優化助手」。**原则**:产品**显示/品牌名** = 「蝦皮圈優化助手」;`cmshopee` 保留为**代码库/仓库/技术代号**,一切**标识符不改**——遵项目「文档与 UI 文案用中文、标识符用英文」规则,且避免非 ASCII 文件名破坏现有用户数据。**改(显示/品牌层)**:① `app/gui/main_window.py` 窗口标题 `setWindowTitle("cmshopee")` → `"蝦皮圈優化助手"`(任务栏/标题栏显示中文品牌);② 文档产品名散文 `docs/00-ai-start-here.md`、`docs/README.md`、`AGENTS.md`(及 vision/brief 若有)把「cmshopee 是一个…」改为「蝦皮圈優化助手(代号 cmshopee)是一个…」,明确品牌名并保留代号;③ 空的 `README.md` 补品牌标题行;④ 可选:`app/gui/__init__.py` PySide6 缺失启动失败提示带上品牌名。**不改(技术标识符,保留 cmshopee)**:`app` 包名、`cmshopee.exe` 打包产物名、`cmshopee.db`(+`-wal/-shm`)/`db_path` 默认值、`cmshopee.spec`、`cmshopee.log`、`config.json`/`config/*.json` 文件名、slug/`chrome_user_data_dir`、仓库目录;`app/editor.py` 注入页面的 JS 全局 `window.__cmshopee_*`(JS 标识符须 ASCII)、`app/ai.py` multipart boundary `----cmshopee-*`、`app/workers.py` `thread._cmshopee_worker`、`app/chrome.py` 快捷方式内部前缀 `cmshopee-`。**决策点**:`app/excel.py` 回写文件名后缀 `_cmshopee回写` 为用户可见产物名——本任务**默认保留**(改动会影响已生成副本命名习惯),如需改「_優化助手回写」另行确认。**影响**:不改 db/exe/spec/包名 → 打包、更新流程、现有用户数据零影响。验收:窗口标题栏/任务栏/README/相关文档显示「蝦皮圈優化助手」;`cmshopee.db`/exe/包名不变;GUI 单测若断言窗口标题须同步更新;`python -m compileall app main.py` + `python -m unittest discover -s tests` 全绿。边界:纯显示/文档层,不改任何文件名、schema、流程、CDP/Shopee 逻辑 | DONE | | T-537 | 品牌名「蝦皮圈優化助手」(显示层中文品牌,cmshopee 保留为代号/标识符) | T-523 | 需求:项目/软件正式定名「蝦皮圈優化助手」。**原则**:产品**显示/品牌名** = 「蝦皮圈優化助手」;`cmshopee` 保留为**代码库/仓库/技术代号**,一切**标识符不改**——遵项目「文档与 UI 文案用中文、标识符用英文」规则,且避免非 ASCII 文件名破坏现有用户数据。**改(显示/品牌层)**:① `app/gui/main_window.py` 窗口标题 `setWindowTitle("cmshopee")` → `"蝦皮圈優化助手"`(任务栏/标题栏显示中文品牌);② 文档产品名散文 `docs/00-ai-start-here.md`、`docs/README.md`、`AGENTS.md`(及 vision/brief 若有)把「cmshopee 是一个…」改为「蝦皮圈優化助手(代号 cmshopee)是一个…」,明确品牌名并保留代号;③ 空的 `README.md` 补品牌标题行;④ 可选:`app/gui/__init__.py` PySide6 缺失启动失败提示带上品牌名。**不改(技术标识符,保留 cmshopee)**:`app` 包名、`cmshopee.exe` 打包产物名、`cmshopee.db`(+`-wal/-shm`)/`db_path` 默认值、`cmshopee.spec`、`cmshopee.log`、`config.json`/`config/*.json` 文件名、slug/`chrome_user_data_dir`、仓库目录;`app/editor.py` 注入页面的 JS 全局 `window.__cmshopee_*`(JS 标识符须 ASCII)、`app/ai.py` multipart boundary `----cmshopee-*`、`app/workers.py` `thread._cmshopee_worker`、`app/chrome.py` 快捷方式内部前缀 `cmshopee-`。**决策点**:`app/excel.py` 回写文件名后缀 `_cmshopee回写` 为用户可见产物名——本任务**默认保留**(改动会影响已生成副本命名习惯),如需改「_優化助手回写」另行确认。**影响**:不改 db/exe/spec/包名 → 打包、更新流程、现有用户数据零影响。验收:窗口标题栏/任务栏/README/相关文档显示「蝦皮圈優化助手」;`cmshopee.db`/exe/包名不变;GUI 单测若断言窗口标题须同步更新;`python -m compileall app main.py` + `python -m unittest discover -s tests` 全绿。边界:纯显示/文档层,不改任何文件名、schema、流程、CDP/Shopee 逻辑 | DONE |
| T-538 | 打包产物用户数据收进 `data/` 子文件夹(便携方案 1) | T-524 | 问题:当前 onedir 产物把用户数据(`config.json`、`config/ai_models.json`、`config/cmhub.json`、`cmshopee.db(+-wal/-shm)`、`chrome_user_data_dir/`、`images/`、`logs/`、`prompts/`、`title_prompt.txt`)生成在 exe 顶层,和程序文件(`cmshopee.exe`+`_internal/`)混放:更新易误删数据、放进 Program Files 只读目录会写失败/被 VirtualStore 重定向、程序与数据关注点混杂。方案(保持便携):① **统一数据根**——所有用户数据落到 exe 同级的单个 `data/` 子文件夹;`app/appconfig.py` 用显式 `data_dir`(默认 `<exe目录>/data`,源码运行时可为项目根/`data`)解析并返回绝对路径,**替代现在 `main.py` 的 chdir**(chdir 影响全进程相对路径/文件对话框默认目录,脆弱);文件名保持不变(仍是 `cmshopee.db` 等),只是移入 `data/`。② **旧布局迁移**——新版首次运行若在 exe 顶层发现旧的 `config.json`/`cmshopee.db` 等,自动迁移进 `data/`(或明确提示手动迁移),不得对老用户生成空 `data/` 丢失原数据。③ **可写性检测**——启动时若 `data_dir` 不可写(如放在 Program Files),弹清晰错误「请把程序放到可写目录,勿放 Program Files」并优雅退出,不静默失败。④ **发布只含程序**——`cmshopee.spec`/`scripts/build_exe.ps1` 产物仅 `cmshopee.exe`+`_internal/`,构建检查确保不打包 `data/` 或任何用户数据;发布 zip 不含 `data/`,从而「整包解压覆盖」也安全(只覆盖程序、不碰 `data/`)。⑤ **更新流程前提**——原地更新=换程序留 `data/`,依赖:DB 迁移幂等且只向前(现 `init_db`+`ALTER TABLE ADD COLUMN`)、`config` 向前兼容(`load_config` 补默认值)、`_internal/` 整体替换不合并。同步更新 `docs/packaging.md`(目录结构、首次运行数据位置、§5 更新步骤简化为「覆盖 exe+_internal,`data/` 不动」)。测试:`test_appconfig` 断言各路径解析进 `data_dir`、可写性检测行为、旧布局迁移;`test_packaging` 断言产物不含 `data/`/用户数据。边界:只改路径解析层(`appconfig` 路径 + `main.py` 数据根 + 迁移/可写检测)、spec/构建脚本、打包文档;不改文件名、DB schema、业务逻辑、AI/Excel、CDP/Shopee 流程 | DOING | | T-538 | 打包产物用户数据收进 `data/` 子文件夹(便携方案 1) | T-524 | 问题:当前 onedir 产物把用户数据(`config.json`、`config/ai_models.json`、`config/cmhub.json`、`cmshopee.db(+-wal/-shm)`、`chrome_user_data_dir/`、`images/`、`logs/`、`prompts/`、`title_prompt.txt`)生成在 exe 顶层,和程序文件(`cmshopee.exe`+`_internal/`)混放:更新易误删数据、放进 Program Files 只读目录会写失败/被 VirtualStore 重定向、程序与数据关注点混杂。方案(保持便携):① **统一数据根**——所有用户数据落到 exe 同级的单个 `data/` 子文件夹;`app/appconfig.py` 用显式 `data_dir`(默认 `<exe目录>/data`,源码运行时可为项目根/`data`)解析并返回绝对路径,**替代现在 `main.py` 的 chdir**(chdir 影响全进程相对路径/文件对话框默认目录,脆弱);文件名保持不变(仍是 `cmshopee.db` 等),只是移入 `data/`。② **旧布局迁移**——新版首次运行若在 exe 顶层发现旧的 `config.json`/`cmshopee.db` 等,自动迁移进 `data/`(或明确提示手动迁移),不得对老用户生成空 `data/` 丢失原数据。③ **可写性检测**——启动时若 `data_dir` 不可写(如放在 Program Files),弹清晰错误「请把程序放到可写目录,勿放 Program Files」并优雅退出,不静默失败。④ **发布只含程序**——`cmshopee.spec`/`scripts/build_exe.ps1` 产物仅 `cmshopee.exe`+`_internal/`,构建检查确保不打包 `data/` 或任何用户数据;发布 zip 不含 `data/`,从而「整包解压覆盖」也安全(只覆盖程序、不碰 `data/`)。⑤ **更新流程前提**——原地更新=换程序留 `data/`,依赖:DB 迁移幂等且只向前(现 `init_db`+`ALTER TABLE ADD COLUMN`)、`config` 向前兼容(`load_config` 补默认值)、`_internal/` 整体替换不合并。同步更新 `docs/packaging.md`(目录结构、首次运行数据位置、§5 更新步骤简化为「覆盖 exe+_internal,`data/` 不动」)。测试:`test_appconfig` 断言各路径解析进 `data_dir`、可写性检测行为、旧布局迁移;`test_packaging` 断言产物不含 `data/`/用户数据。边界:只改路径解析层(`appconfig` 路径 + `main.py` 数据根 + 迁移/可写检测)、spec/构建脚本、打包文档;不改文件名、DB schema、业务逻辑、AI/Excel、CDP/Shopee 流程 | DOING |
+1
View File
@@ -110,6 +110,7 @@
- T-523a 已完成:②点击「开始生成」、③点击「检查本轮更新」或「开始更新」时,先清空对应界面的旧日志显示并写入本轮开始摘要;运行中只追加本轮日志。历史 `run_logs/run_log_events` 和本地 `logs/` 不删除、不自动混入当前运行界面。 - T-523a 已完成:②点击「开始生成」、③点击「检查本轮更新」或「开始更新」时,先清空对应界面的旧日志显示并写入本轮开始摘要;运行中只追加本轮日志。历史 `run_logs/run_log_events` 和本地 `logs/` 不删除、不自动混入当前运行界面。
- T-404b 已完成:`open_product()` 在导航商品详情页前后安装 toast 捕获,等待详情页关键元素超时时会读取最近 `.eds-toasts`/toast/message 节点及页面缓存,把明确商品失效/商品不存在/无权限类 toast 上浮为 `商品失效:<原始toast>`;如果失败发生在 `open_product()` 内部,本轮自动新建的失败 tab 会关闭,复用用户已有 tab 不关闭;①列表“阶段”列只在这类明确失效错误时显示“商品失效”,底层仍保持 `stage=imported/status=failed`,其他打开失败仍显示“失败”。 - T-404b 已完成:`open_product()` 在导航商品详情页前后安装 toast 捕获,等待详情页关键元素超时时会读取最近 `.eds-toasts`/toast/message 节点及页面缓存,把明确商品失效/商品不存在/无权限类 toast 上浮为 `商品失效:<原始toast>`;如果失败发生在 `open_product()` 内部,本轮自动新建的失败 tab 会关闭,复用用户已有 tab 不关闭;①列表“阶段”列只在这类明确失效错误时显示“商品失效”,底层仍保持 `stage=imported/status=failed`,其他打开失败仍显示“失败”。
- T-524 已完成:新增 PyInstaller onedir 打包配置、构建脚本和发布目录校验;打包版启动时工作目录切到 `cmshopee.exe` 所在目录,首次运行在 exe 同级生成/使用本地配置、DB、图片、日志、登录态和提示词;发布包不内置这些本地数据,用户后续更新采用关闭程序后覆盖程序文件、保留本地数据的方式。 - T-524 已完成:新增 PyInstaller onedir 打包配置、构建脚本和发布目录校验;打包版启动时工作目录切到 `cmshopee.exe` 所在目录,首次运行在 exe 同级生成/使用本地配置、DB、图片、日志、登录态和提示词;发布包不内置这些本地数据,用户后续更新采用关闭程序后覆盖程序文件、保留本地数据的方式。
- T-541 已完成:`MainWindow` 启动时通过 `_fit_and_center_window()` 读取 `QApplication.primaryScreen().availableGeometry()`,按可用屏幕限制初始尺寸、居中并夹进屏幕;首次 `showEvent` 后再应用一次,保证打包版在 Windows 10 VM/小分辨率环境标题栏完整可见、可拖动,不保存历史坏坐标。
- Phase 7 cmhub 网关对接已完成 T-526~T-535;②生成阶段已支持按标题/封面缺失组件增量生成,②重置生成结果已支持多选/筛选范围和按标题/封面组件重置,cmhub 标题请求读取等待固定 600 秒。下一步按看板回到 T-525 ruff。 - Phase 7 cmhub 网关对接已完成 T-526~T-535;②生成阶段已支持按标题/封面缺失组件增量生成,②重置生成结果已支持多选/筛选范围和按标题/封面组件重置,cmhub 标题请求读取等待固定 600 秒。下一步按看板回到 T-525 ruff。
- T-301/T-303 已完成通用 HTTP AI 接口、批量生成编排和 GUI 接入 mock 单测;真实 AI 生成还需要在 `config/ai_models.json` 填入可用 url/model/api_key 后做一次成本可控的小样本实测;URL 可填完整 endpoint 或 OpenAI-compatible base URL。 - T-301/T-303 已完成通用 HTTP AI 接口、批量生成编排和 GUI 接入 mock 单测;真实 AI 生成还需要在 `config/ai_models.json` 填入可用 url/model/api_key 后做一次成本可控的小样本实测;URL 可填完整 endpoint 或 OpenAI-compatible base URL。
+1
View File
@@ -95,6 +95,7 @@ powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1
- `dist\cmshopee\cmshopee.exe` 存在。 - `dist\cmshopee\cmshopee.exe` 存在。
- `dist\cmshopee\` 中没有第三节列出的本地数据。 - `dist\cmshopee\` 中没有第三节列出的本地数据。
- 在干净目录首次启动时能生成默认配置并进入 GUI。 - 在干净目录首次启动时能生成默认配置并进入 GUI。
- 在目标 Windows 10/11 机器或虚拟机上启动 release exe 后,主窗口标题栏完整可见,左边缘不出屏,用户能用标题栏拖动窗口;小分辨率环境不得出现窗口卡在左上角且标题栏不可拖动的问题(见 T-541)。
涉及 Shopee/CDP 的真实更新能力,仍按任务文档要求用测试商品做人工回归;打包任务本身不新增自动绕过登录、验证码或风控的能力。 涉及 Shopee/CDP 的真实更新能力,仍按任务文档要求用测试商品做人工回归;打包任务本身不新增自动绕过登录、验证码或风控的能力。
+38
View File
@@ -227,3 +227,41 @@ T-530 已实现:保存和请求前都会把 Base URL 规整为网关根,去
已修复:T-105b 已把④「启动登录」改成幂等流程。先检查该账号 `debug_port` 是否已有 CDP 响应;已响应时不再 `Popen`,而是复用现有 Chrome/CDP 并打开或激活 `https://<region_host>/portal/` 登录/卖家中心 tab;未响应时才新启动 Chrome。该修复不自动登录、不填密码、不绕过验证码,也不改变①/③预检“不自动启动 Chrome”的规则。 已修复:T-105b 已把④「启动登录」改成幂等流程。先检查该账号 `debug_port` 是否已有 CDP 响应;已响应时不再 `Popen`,而是复用现有 Chrome/CDP 并打开或激活 `https://<region_host>/portal/` 登录/卖家中心 tab;未响应时才新启动 Chrome。该修复不自动登录、不填密码、不绕过验证码,也不改变①/③预检“不自动启动 Chrome”的规则。
验证方式:同一账号第一次点击「启动登录」应显示“Chrome 已启动,请人工登录”;保持该账号 Chrome 不关闭,再点一次应显示“该账号 Chrome 已打开,已复用现有窗口”,且不会新增 Chrome 进程。 验证方式:同一账号第一次点击「启动登录」应显示“Chrome 已启动,请人工登录”;保持该账号 Chrome 不关闭,再点一次应显示“该账号 Chrome 已打开,已复用现有窗口”,且不会新增 Chrome 进程。
## 打包版窗口左上角显示不全,标题栏不可拖动
### 现象
在 Windows 10 虚拟机或小分辨率环境运行打包后的 `dist/cmshopee/cmshopee.exe` / release 包后,主窗口出现在屏幕左上角,左边和标题栏显示不全,看不到「蝦皮圈優化助手」标题,导致无法用鼠标拖动窗口到中间。
### 原因
旧逻辑只设置固定窗口大小:
```python
self.resize(1180, 760)
```
没有按当前屏幕可用区域限制初始尺寸、居中显示,也没有保证窗口标题栏完整落在屏幕内。虚拟机、小分辨率、高 DPI、任务栏占用较多或多显示器切换环境下更容易暴露该问题;PyInstaller 打包不是根因。
### 临时处理
如果已经遇到窗口无法拖动:
1. 按 `Alt + Space`。
2. 按 `M` 选择移动。
3. 用方向键把窗口移回屏幕中间,再按回车。
也可以尝试 `Win + 方向键` 贴靠/恢复窗口,或先调高虚拟机分辨率。
### 当前修复(T-541 后)
T-541 已在主窗口启动时读取 `QApplication.primaryScreen().availableGeometry()`:
- 大屏保持接近 `1180x760`。
- 小屏自动缩小到可用区域内。
- `resize()` 后按可用区域居中 `move()`。
- 确保窗口左上角不小于可用区域左上角,标题栏完整可见、可拖动。
- 不保存坏的历史窗口坐标,避免下次继续打开到屏幕外。
修复后需要在普通桌面和 Windows 10 虚拟机/小分辨率环境各启动一次打包版,确认标题栏完整可见。
+16
View File
@@ -1271,3 +1271,19 @@
- 代码:`app/gui/main_window.py` 主窗口标题改为「蝦皮圈優化助手」;`app/gui/__init__.py` 的 PySide6 缺失启动提示带上中文品牌名;`tests/test_gui.py` 补充窗口标题断言。 - 代码:`app/gui/main_window.py` 主窗口标题改为「蝦皮圈優化助手」;`app/gui/__init__.py` 的 PySide6 缺失启动提示带上中文品牌名;`tests/test_gui.py` 补充窗口标题断言。
- 边界:只改显示层和文档层;`cmshopee` 继续作为仓库、包名、exe、数据库、日志、配置、回写后缀和兼容标识符保留;未改任何文件名、schema、AI、Excel、DB、CDP 或 Shopee 更新流程。 - 边界:只改显示层和文档层;`cmshopee` 继续作为仓库、包名、exe、数据库、日志、配置、回写后缀和兼容标识符保留;未改任何文件名、schema、AI、Excel、DB、CDP 或 Shopee 更新流程。
- 验证:`python -m py_compile app\gui\main_window.py app\gui\__init__.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(95 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过;`git diff --check` 通过。 - 验证:`python -m py_compile app\gui\main_window.py app\gui\__init__.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(95 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过;`git diff --check` 通过。
## 【2026-07-07】T-541 文档 · 打包版窗口左上角显示不全
- 现象:运行打包后的 `dist/cmshopee/cmshopee.exe` / release 包时,Windows 10 虚拟机或小分辨率环境可能出现主窗口贴到屏幕左上角、左边和标题栏显示不全,用户看不到品牌标题且无法拖动窗口到中间。
- 原因:`MainWindow.__init__()` 当前只 `resize(1180, 760)`,没有读取 `QApplication.primaryScreen().availableGeometry()` 限制初始尺寸、居中显示,也没有保证窗口标题栏完整落在可用屏幕内。
- 文档:`docs/06-tasks.md` 新增 T-541;`docs/troubleshooting.md` 增加临时处理(`Alt+Space` → `M` → 方向键移动)和正式修复方案;`docs/packaging.md` 打包验收清单新增标题栏完整可见/可拖动检查;`docs/current-state.md` 记录 T-541 修复口径。
- 方案:后续代码在主窗口启动时根据屏幕可用区域限制尺寸并居中,保证标题栏可见;小屏继续适配,不保存坏的历史窗口坐标。
- 边界:本轮只更新文档,未改 GUI 代码、打包脚本、业务逻辑、DB、AI、Excel、CDP 或 Shopee 流程。
## 【2026-07-07】T-541 完成 · 打包版主窗口初始位置与小屏适配
- 状态:DONE
- 代码:`app/gui/main_window.py` 新增 `_fit_and_center_window()`,启动时读取 `QApplication.primaryScreen().availableGeometry()`,将主窗口尺寸限制在可用屏幕范围内并居中;首次 `showEvent` 后再应用一次,避免打包版在 Windows 10 VM/小分辨率环境标题栏出屏。
- 文档:`docs/06-tasks.md` 将 T-541 标记为 DONE;`docs/troubleshooting.md`、`docs/packaging.md`、`docs/current-state.md` 更新为当前修复与打包验收口径。
- 测试:`tests/test_gui.py` 新增小屏和大屏 mock geometry 单测,覆盖尺寸限制与居中位置;`scripts/build_exe.ps1` 已重新构建 release,确认 T-541 合入后仍可打包。
- 边界:不保存/恢复窗口历史坐标;未改业务流程、DB、AI、Excel、CDP/Shopee、打包产物结构或用户数据路径。
+38 -1
View File
@@ -17,7 +17,7 @@ from app import accounts, ai, appconfig, db, prompts
if gui.QT_IMPORT_ERROR is not None: if gui.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装") raise unittest.SkipTest("PySide6 未安装")
from PySide6.QtCore import QItemSelectionModel from PySide6.QtCore import QItemSelectionModel, QRect
from PySide6.QtGui import QTextCursor from PySide6.QtGui import QTextCursor
from PySide6.QtWidgets import QApplication, QCheckBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView from PySide6.QtWidgets import QApplication, QCheckBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
@@ -39,6 +39,7 @@ from app.gui import (
TAB_TITLES, TAB_TITLES,
WriteBackWorker, WriteBackWorker,
) )
from app.gui.main_window import _fit_and_center_window
class GuiTests(TempDirMixin, unittest.TestCase): class GuiTests(TempDirMixin, unittest.TestCase):
@@ -182,6 +183,42 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
def test_main_window_initial_fit_limits_size_on_small_screen(self):
class FakeWindow:
def __init__(self):
self.size = None
self.position = None
def resize(self, width, height):
self.size = (width, height)
def move(self, x, y):
self.position = (x, y)
window = FakeWindow()
_fit_and_center_window(window, available_geometry=QRect(0, 0, 900, 700))
self.assertEqual((820, 620), window.size)
self.assertEqual((40, 40), window.position)
def test_main_window_initial_fit_centers_preferred_size_on_large_screen(self):
class FakeWindow:
def __init__(self):
self.size = None
self.position = None
def resize(self, width, height):
self.size = (width, height)
def move(self, x, y):
self.position = (x, y)
window = FakeWindow()
_fit_and_center_window(window, available_geometry=QRect(100, 50, 1600, 900))
self.assertEqual((1180, 760), window.size)
self.assertEqual((310, 120), window.position)
def test_collect_tab_shows_empty_state_without_accounts(self): def test_collect_tab_shows_empty_state_without_accounts(self):
with self.make_temp_dir() as temp_dir: with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir) cfg = self.make_config(temp_dir)