diff --git a/app/gui/tabs/settings.py b/app/gui/tabs/settings.py index 0ed862f..06d1b6c 100644 --- a/app/gui/tabs/settings.py +++ b/app/gui/tabs/settings.py @@ -82,12 +82,10 @@ class SettingsTab(QWidget): for label, value in self.BACKEND_ITEMS: self.backend_combo.addItem(label, value) self.backend_combo.setVisible(False) - self.gateway_default_button = QPushButton("默认网关") + self.gateway_default_button = QRadioButton("默认网关") self.gateway_default_button.setObjectName("gatewayDefaultButton") - self.gateway_default_button.setCheckable(True) - self.gateway_custom_button = QPushButton("自定义网关") + self.gateway_custom_button = QRadioButton("自定义网关") self.gateway_custom_button.setObjectName("gatewayCustomButton") - self.gateway_custom_button.setCheckable(True) self.gateway_button_group = QButtonGroup(self) self.gateway_button_group.setExclusive(True) self.gateway_button_group.addButton(self.gateway_default_button) @@ -100,11 +98,11 @@ class SettingsTab(QWidget): gateway_selector_layout.addWidget(self.gateway_default_button) gateway_selector_layout.addWidget(self.gateway_custom_button) self.gateway_selector.setStyleSheet( - "QPushButton { min-width: 108px; padding: 6px 12px; border: 1px solid #b8c0ca; " - "background: #f6f8fa; color: #57606a; }" - "QPushButton:first-child { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }" - "QPushButton:last-child { border-left: 0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }" - "QPushButton:checked { background: #0969da; border-color: #0969da; color: white; font-weight: 600; }" + "QRadioButton { min-width: 108px; min-height: 30px; padding: 5px 12px; " + "color: #24292f; spacing: 7px; }" + "QRadioButton:hover { color: #0969da; }" + "QRadioButton:focus { border: 1px solid #0969da; border-radius: 4px; }" + "QRadioButton::indicator { width: 15px; height: 15px; }" ) self.cmhub_base_url_edit = QLineEdit() self.cmhub_base_url_edit.setObjectName("cmhubBaseUrlEdit") diff --git a/docs/routes.md b/docs/routes.md index db459a2..6555e79 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -171,7 +171,7 @@ ## 设置 - 设置页整体布局:内容区居中,左右留白已从 T-506 初始实现缩短到约 40%;实现上用最大内容宽度 + 自适应 margin,而不是写死窗口像素。所有设置组默认响应式 3 列表单:短字段占 1 格,URL/API Key/路径等长字段跨 2 格或 3 格;窄窗口自动降为 2 列/1 列。点击「保存设置」成功后,状态栏显示“设置已保存”,并弹出轻量提示框。T-531 已完成:保存成功会清除未保存标记;保存失败时保留未保存标记并阻止离开。 -- 生成网关(T-677):用具备选中态的「默认网关 / 自定义网关」来源选择器替代历史「AI 后端」下拉。切换只改变待保存状态并立即切换面板,点击「保存设置」才写入 `ai.backend=cmhub/direct`;默认网关的 `data/config/cmhub.json` 与自定义模型的 `data/config/ai_models.json` 始终并存,模型新增、保存、删除仍立即写盘,不受来源待保存语义影响。 +- 生成网关(T-677/T-680):在原「AI 模型配置」标题下使用同行标准单选框「默认网关 / 自定义网关」替代历史「AI 后端」下拉和不明显的分段按钮;启动/放弃未保存更改时按已保存的 `ai.backend=cmhub/direct` 自动勾选对应项,圆点与焦点状态表达当前选择。切换只改变待保存状态并立即切换原有面板,点击「保存设置」才写入配置;默认网关的 `data/config/cmhub.json` 与自定义模型的 `data/config/ai_models.json` 始终并存,模型新增、保存、删除仍立即写盘,不受来源待保存语义影响。除这两个选择控件外,不改变设置页其它组件、表单布局或业务逻辑。 - 默认网关面板展示 Base URL、API Key、生文/生图/图片理解别名、连接超时、批量生成前检查余额、刷新别名和测试连接/查余额;Base URL 输入框旁提示“只填网关根,如 https://host”,保存/刷新前规整掉 `/api`、`/api/v1` 或其它路径。测试连接成功优先显示 `账号「<账号名>」连接默认网关成功`,邮箱脱敏;没有账号信息时显示「默认网关连接成功」。Key 只本地明文保存、UI 打码显示,不进入日志或导出。 - 自定义网关复用既有模型列表、模型详情、默认文本模型与默认图像模型。文本模型保留 `chat` / `auto`;图片模型固定使用「OpenAI 图片编辑接口」(`images_edits`),②和⑥通过 `/v1/images/edits` 以有序重复 `image[]` 上传,第一张是商品主体图、每次只请求 `n=1` 输出。历史 `chat` / `auto` 图片模型不会被改写或隐藏,但会标示「图片接口不支持」,生成前会被拦截。图片响应只接受 `data[].b64_json` / `data[].url`,不适配聊天响应或服务商私有结构。说明行明确「图片理解仅支持默认网关」。 - 图片模型按钮显示「检查图片配置」,只在本地检查接口类型、URL、模型 ID、密钥和超时,绝不对 `/images/edits` 发送无图片的测试请求;实际可用性必须以②或后续⑥的正常生成确认判断。允许保存不完整配置。②点击生成后、自定义 worker 创建前按本轮标题/封面选择校验所需模型的启用状态、类别、地址、模型 ID、API Key 和接口类型,失败提示去⑤补齐且不发请求、不改任务状态;自定义网关不显示默认网关余额,运行日志和状态栏说明“不计点数,费用由服务商收取”。 diff --git a/docs/tasks/T-680.md b/docs/tasks/T-680.md index dbabf2e..2c6ae21 100644 --- a/docs/tasks/T-680.md +++ b/docs/tasks/T-680.md @@ -3,7 +3,7 @@ id: T-680 title: 设置页生成网关改为同行单选框 phase: 5 deps: [T-677] -status: TODO +status: DONE created: 2026-07-21 --- @@ -62,4 +62,7 @@ git diff --check ## 执行记录 -(做完在这里写:控件替换范围、原型同步、GUI 验证和回归结果。) +- 将 `gateway_selector` 内原有的两个可勾选 `QPushButton` 替换为同一 `QButtonGroup` 管理的两个 `QRadioButton`,保留对象名、同行顺序、零间距布局、每项最小宽度以及原有选中、保存、放弃未保存更改和面板切换方法。 +- 选中状态改由标准圆点、鼠标悬停色和键盘焦点边框表达;加载 `cmhub/direct`、鼠标点击和方向键切换均保持互斥。没有改动其它设置输入框、label、面板结构、表单网格或业务配置字段。 +- 已将 `docs/ui/settings-gateway-radio-prototype-v1.html` 收缩为原位置的同行单选框方案,移除上下重排、侧栏、运行任务说明、深色预览和其它输入框改版;更新设置路由与 UI 索引。 +- 测试:更新 `tests/test_gui.py`,覆盖控件类型、同行布局、已保存默认来源、方向键互斥切换、待保存状态、保存和放弃回填。验证:`py -3.10 -m unittest tests.test_gui`(211 项)、HTML 解析、内嵌 JavaScript 语法检查、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check` 均通过;完整 `py -3.10 -m unittest discover -s tests` 通过(651 项)。 diff --git a/docs/ui/README.md b/docs/ui/README.md index b5c24b8..d99df25 100644 --- a/docs/ui/README.md +++ b/docs/ui/README.md @@ -19,6 +19,7 @@ | [tab6-suite-prompt-dialog-T637.svg](tab6-suite-prompt-dialog-T637.svg) | 商品套图(T-637 提案):标题行「AI帮写/取消」紧随 label、最右新增「提示词设置」;提示词设置弹窗——左编辑基础模板(行序对齐调研笔记组装顺序,套图名称+补充描述、插入变量菜单、占位符校验、只读规则占位符内联)、右只读实时最终预览(预览分类下拉)、保存/恢复默认共用原子写盘;第一版不做同类多张序号/差异化 | | [tab6-suite-gateway-T679b.svg](tab6-suite-gateway-T679b.svg) | 商品套图(T-679b 已实现):自定义网关生成确认展示主图/参考图、近似比例尺寸、服务商费用与主体一致性边界;生成中锁定当前任务输入,停止等待已发出同步请求返回,同时仍可切换其它任务与查看历史/结果 | | [tab6-suite-recovery-T679c.svg](tab6-suite-recovery-T679c.svg) | 商品套图(T-679c 已实现):结果卡和历史按已保存 job 显示默认网关/自定义网关;默认网关任务可继续查询,直连失败图只能手动重新生成并再次确认可能重复收费 | +| [settings-gateway-radio-prototype-v1.html](settings-gateway-radio-prototype-v1.html) | 设置(T-680 已实现):在原 AI 模型配置标题下保留同行位置,仅以原生单选框替换默认网关/自定义网关的分段按钮;可在浏览器中模拟切换、待保存状态和保存,不改其它设置组件 | | [workbench-redesign-v1.html](workbench-redesign-v1.html) | 工作台视觉提案(HTML 可交互原型):以当前 6 Tab 信息架构为基础,统一「数据准备区→表格命令区→表格→状态反馈区→完成任务区」页面骨架;命令按作用域分区(导入/筛选/选择集/工作流完成),表格补齐当前行、选择集、三态全选与排序契约;范围确认、部分成功、取消中间态和破坏性确认按干扰层级分配到对话框、信息条与 Toast;覆盖浅色/深色/高对比度近似、键盘(Tab/方向键/F6/Ctrl+F/F5)与 Windows 断点。商品状态相关交互已对齐 T-662b/c/d、T-663~T-665、T-667 定稿(无状态列与状态下拉筛选,③无强制更新入口)。颜色 Token 取自 `app/gui/widgets.py`。使用模拟数据,不接生产接口;页脚「原型说明与验收边界」列出假设与不可仅靠 HTML 验收的项。 | 应用图标(非效果图,是随程序发布的资源):`app/assets/cmshopee-logo.svg` 是可编辑母版,`cmshopee.ico`(16–256 共 9 档)用于 exe 图标和 GUI 窗口左上角,`cmshopee-256.png` 为预览与非 Windows 回退。三者由 `py -3.10 scripts/gen_logo.py` 从同一份几何定义生成,改样式请改脚本后重新生成,不要单独手改某一个文件。 diff --git a/docs/ui/settings-gateway-radio-prototype-v1.html b/docs/ui/settings-gateway-radio-prototype-v1.html new file mode 100644 index 0000000..a005a22 --- /dev/null +++ b/docs/ui/settings-gateway-radio-prototype-v1.html @@ -0,0 +1,364 @@ + + + + + + 设置 - 生成网关单选框原型 + + + +
+
蝦皮圈優化助手
+ + +
+
+

AI 模型配置

+
+
+ 生成网关 + + +
+
+ +
+
+ + +

只填默认网关根地址,不要带 /api 或 /api/v1。

+ + + + + + + + + + + + + 批量生成前检查余额 + +
+

默认网关的输入框、顺序和保存规则保持现有实现不变。

+
+ + + +
+

AI 生成参数

+
+ 标题并发
+ 图片并发
+ 失败重试
+
+ +
+

路径与端口

+
+ Chrome 路径
+ 默认端口
+ 端口范围
+
+ +
+ + + 当前设置已保存 +
+
+
+
+ + + + diff --git a/tests/test_gui.py b/tests/test_gui.py index 086e86e..1df99c5 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -42,6 +42,7 @@ from PySide6.QtWidgets import ( QPlainTextEdit, QProgressBar, QPushButton, + QRadioButton, QTableView, ) @@ -3055,11 +3056,22 @@ class GuiTests(TempDirMixin, unittest.TestCase): ) self.addCleanup(tab.close) + self.assertIsInstance(tab.gateway_default_button, QRadioButton) + self.assertIsInstance(tab.gateway_custom_button, QRadioButton) + self.assertEqual(0, tab.gateway_selector.layout().spacing()) + self.assertEqual(2, tab.gateway_selector.layout().count()) self.assertTrue(tab.gateway_default_button.isChecked()) self.assertFalse(tab.gateway_custom_button.isChecked()) self.assertFalse(tab.cmhub_panel.isHidden()) self.assertTrue(tab.model_picker_panel.isHidden()) + tab.show() + self.app.processEvents() + tab.gateway_default_button.setFocus() + QTest.keyClick(tab.gateway_default_button, Qt.Key_Right) + self.assertTrue(tab.gateway_custom_button.isChecked()) + self.assertFalse(tab.gateway_default_button.isChecked()) + tab.gateway_custom_button.setChecked(True) self.assertTrue(tab.gateway_custom_button.isChecked()) self.assertFalse(tab.gateway_default_button.isChecked()) @@ -3074,6 +3086,12 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assertEqual("direct", appconfig.load_config(cfg["config_path"])["ai"]["backend"]) + tab.gateway_default_button.setChecked(True) + self.assertTrue(tab.is_dirty()) + self.assertTrue(tab.discard_unsaved_changes()) + self.assertFalse(tab.gateway_default_button.isChecked()) + self.assertTrue(tab.gateway_custom_button.isChecked()) + self.assert_removed(temp_dir) def test_settings_tab_cmhub_alias_refresh_filters_unpriced_and_keeps_saved(self):