feat(settings): use radio buttons for gateway

This commit is contained in:
chengma
2026-07-21 09:07:04 +08:00
parent 4217f855ac
commit 415efe91a8
6 changed files with 396 additions and 12 deletions
+1
View File
@@ -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>