feat(settings): use radio buttons for gateway
This commit is contained in:
+1
-1
@@ -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 和接口类型,失败提示去⑤补齐且不发请求、不改任务状态;自定义网关不显示默认网关余额,运行日志和状态栏说明“不计点数,费用由服务商收取”。
|
||||
|
||||
+5
-2
@@ -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 项)。
|
||||
|
||||
@@ -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` 从同一份几何定义生成,改样式请改脚本后重新生成,不要单独手改某一个文件。
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>设置 - 生成网关单选框原型</title>
|
||||
<style>
|
||||
:root {
|
||||
--canvas: #f6f8fa;
|
||||
--surface: #ffffff;
|
||||
--surface-subtle: #f6f8fa;
|
||||
--text: #24292f;
|
||||
--muted: #57606a;
|
||||
--border: #d0d7de;
|
||||
--border-strong: #8c959f;
|
||||
--accent: #0969da;
|
||||
--accent-soft: #ddf4ff;
|
||||
--warning: #9a6700;
|
||||
--warning-soft: #fff8c5;
|
||||
--radius: 4px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
min-width: 640px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: var(--canvas);
|
||||
color: var(--text);
|
||||
font: 14px/1.45 "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
button, input, select { font: inherit; }
|
||||
button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
outline: 3px solid rgba(9, 105, 218, .35);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.window {
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
min-height: 720px;
|
||||
margin: 20px auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 12px 30px rgba(31, 35, 40, .12);
|
||||
overflow: hidden;
|
||||
}
|
||||
.titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
background: #203451;
|
||||
color: #ffffff;
|
||||
font-weight: 650;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 7px 14px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
.tab {
|
||||
min-width: 104px;
|
||||
min-height: 34px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tab[aria-selected="true"] {
|
||||
border-color: var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--accent);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.settings-scroll { padding: 28px 18px 18px; }
|
||||
.settings-panel { width: min(920px, 100%); margin: 0 auto; }
|
||||
.section-title {
|
||||
margin: 0 0 8px;
|
||||
padding-top: 4px;
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.gateway-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-height: 42px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.gateway-selector fieldset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
.gateway-radio {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 108px;
|
||||
min-height: 30px;
|
||||
padding: 5px 12px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.gateway-radio:hover { color: var(--accent); }
|
||||
.gateway-radio input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 7px 0 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
.gateway-radio:has(input:focus-visible) {
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.section-gap { height: 14px; }
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 104px minmax(0, 1fr) 104px minmax(0, 1fr) 104px minmax(0, 1fr);
|
||||
gap: 8px 18px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
.form-grid label { color: var(--text); }
|
||||
.form-grid input, .form-grid select {
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.span-wide { grid-column: span 3; }
|
||||
.helper {
|
||||
grid-column: 2 / -1;
|
||||
margin: -2px 0 2px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.panel-note {
|
||||
margin: 8px 0 0 122px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.hidden { display: none; }
|
||||
|
||||
.divider { height: 1px; margin: 18px 0; background: var(--border); }
|
||||
.placeholder-section {
|
||||
display: grid;
|
||||
grid-template-columns: 104px minmax(0, 1fr) 104px minmax(0, 1fr) 104px minmax(0, 1fr);
|
||||
gap: 8px 18px;
|
||||
align-items: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
.placeholder-input {
|
||||
min-height: 30px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
.save-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.button {
|
||||
min-height: 30px;
|
||||
min-width: 86px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-subtle);
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.button:hover { background: #eef4ff; }
|
||||
.button.primary { border-color: var(--accent); background: var(--accent); color: #ffffff; }
|
||||
.unsaved {
|
||||
color: var(--warning);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status {
|
||||
margin-left: auto;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.window { width: 100%; min-height: 100vh; margin: 0; border: 0; border-radius: 0; }
|
||||
.tabs { overflow-x: auto; }
|
||||
.form-grid, .placeholder-section { grid-template-columns: 96px minmax(180px, 1fr); }
|
||||
.form-grid label:nth-of-type(n+2), .placeholder-section span:nth-of-type(n+2) { grid-column: 1; }
|
||||
.form-grid input, .form-grid select, .placeholder-input { grid-column: 2; }
|
||||
.span-wide { grid-column: 2; }
|
||||
.helper { grid-column: 2; }
|
||||
.panel-note { margin-left: 104px; }
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.gateway-radio:has(input:checked) { color: HighlightText; background: Highlight; }
|
||||
.gateway-radio:has(input:focus-visible) { outline: 2px solid Highlight; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="window">
|
||||
<header class="titlebar">蝦皮圈優化助手</header>
|
||||
<nav class="tabs" role="tablist" aria-label="主模块">
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">① 导入采集</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">② AI生成</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">③ 更新蝦皮</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">账号管理</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="true">设置</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">商品套图</button>
|
||||
</nav>
|
||||
|
||||
<main class="settings-scroll">
|
||||
<div class="settings-panel">
|
||||
<h1 class="section-title">AI 模型配置</h1>
|
||||
<div class="gateway-selector">
|
||||
<fieldset>
|
||||
<legend class="visually-hidden">生成网关</legend>
|
||||
<label class="gateway-radio">
|
||||
<input type="radio" name="gateway" value="cmhub" checked>
|
||||
默认网关
|
||||
</label>
|
||||
<label class="gateway-radio">
|
||||
<input type="radio" name="gateway" value="direct">
|
||||
自定义网关
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<section id="cmhubPanel" aria-label="默认网关配置">
|
||||
<div class="form-grid">
|
||||
<label for="baseUrl">网关 Base URL</label>
|
||||
<input class="span-wide" id="baseUrl" value="https://cmhub.example.com">
|
||||
<p class="helper">只填默认网关根地址,不要带 /api 或 /api/v1。</p>
|
||||
|
||||
<label for="apiKey">API Key</label>
|
||||
<input id="apiKey" type="password" value="cmhub-local-key">
|
||||
<label for="titleAlias">标题生成别名</label>
|
||||
<select id="titleAlias"><option>title-standard</option></select>
|
||||
<label for="imageAlias">图片生成别名</label>
|
||||
<select id="imageAlias"><option>image-hd</option></select>
|
||||
|
||||
<label for="visionAlias">图片理解别名</label>
|
||||
<select id="visionAlias"><option>vision-standard</option></select>
|
||||
<label for="connectTimeout">连接超时</label>
|
||||
<input id="connectTimeout" value="66">
|
||||
<span>批量生成前检查余额</span>
|
||||
<label><input type="checkbox"> 启用</label>
|
||||
</div>
|
||||
<p class="panel-note">默认网关的输入框、顺序和保存规则保持现有实现不变。</p>
|
||||
</section>
|
||||
|
||||
<section id="directPanel" class="hidden" aria-label="自定义网关模型配置">
|
||||
<div class="form-grid">
|
||||
<label for="modelList">模型列表</label>
|
||||
<select class="span-wide" id="modelList"><option>图片编辑模型 · 图像</option></select>
|
||||
<p class="helper">选择、保存或删除模型的原有交互保持不变。</p>
|
||||
|
||||
<label for="modelName">模型名称</label>
|
||||
<input id="modelName" value="图片编辑模型">
|
||||
<label for="modelId">模型 ID</label>
|
||||
<input id="modelId" value="image-edit">
|
||||
<label for="modelUrl">网址</label>
|
||||
<input id="modelUrl" value="https://example.com/v1">
|
||||
</div>
|
||||
<p class="panel-note">此处仅按现有面板显示,不新增字段或状态组件。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h2 class="section-title">AI 生成参数</h2>
|
||||
<div class="placeholder-section" aria-label="未修改的生成参数示意">
|
||||
<span>标题并发</span><div class="placeholder-input"></div>
|
||||
<span>图片并发</span><div class="placeholder-input"></div>
|
||||
<span>失败重试</span><div class="placeholder-input"></div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h2 class="section-title">路径与端口</h2>
|
||||
<div class="placeholder-section" aria-label="未修改的路径与端口示意">
|
||||
<span>Chrome 路径</span><div class="placeholder-input"></div>
|
||||
<span>默认端口</span><div class="placeholder-input"></div>
|
||||
<span>端口范围</span><div class="placeholder-input"></div>
|
||||
</div>
|
||||
|
||||
<div class="save-row">
|
||||
<button class="button primary" id="saveButton" type="button">保存设置</button>
|
||||
<span class="unsaved hidden" id="unsavedLabel">● 未保存更改</span>
|
||||
<span class="status" id="statusText" role="status" aria-live="polite">当前设置已保存</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const radios = [...document.querySelectorAll('input[name="gateway"]')];
|
||||
const cmhubPanel = document.getElementById("cmhubPanel");
|
||||
const directPanel = document.getElementById("directPanel");
|
||||
const unsavedLabel = document.getElementById("unsavedLabel");
|
||||
const statusText = document.getElementById("statusText");
|
||||
const saveButton = document.getElementById("saveButton");
|
||||
const names = { cmhub: "默认网关", direct: "自定义网关" };
|
||||
let saved = "cmhub";
|
||||
let selected = "cmhub";
|
||||
|
||||
function render() {
|
||||
const dirty = selected !== saved;
|
||||
radios.forEach((radio) => { radio.checked = radio.value === selected; });
|
||||
cmhubPanel.classList.toggle("hidden", selected !== "cmhub");
|
||||
directPanel.classList.toggle("hidden", selected !== "direct");
|
||||
unsavedLabel.classList.toggle("hidden", !dirty);
|
||||
statusText.textContent = dirty
|
||||
? `待保存:${names[selected]}`
|
||||
: `当前使用${names[saved]}`;
|
||||
}
|
||||
|
||||
radios.forEach((radio) => {
|
||||
radio.addEventListener("change", () => {
|
||||
if (radio.checked) {
|
||||
selected = radio.value;
|
||||
render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
saveButton.addEventListener("click", () => {
|
||||
saved = selected;
|
||||
render();
|
||||
statusText.textContent = `设置已保存,当前使用${names[saved]}`;
|
||||
});
|
||||
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user