Files
cmshoppe/docs/ui/workbench-redesign-v1.html
T
chengmaandClaude Fable 5 ed1d8ee764 docs(ui): apply T-677 gateway source switch to workbench prototype
⑤ 设置的网关分区按 T-677 改为生成来源二选一:

- 分区标题「cmhub 网关」改为「生成网关」并保留,新增互斥选中态的
  「默认网关 / 自定义网关」分段选择器(radiogroup,表达状态而非动作)。
- 双面板显隐:默认网关面板含网关地址、API Key、生文/生图/图片理解别名、
  连接超时、测试连接·查余额、批量生成前检查余额;自定义网关面板含模型
  列表、模型详情(名称/类别/接口地址/模型ID/API Key/接口类型/超时/启用)、
  独立的测试按钮与结果标签、默认文本与默认图像模型选择。
- 保存语义区分:来源切换只标记未保存,点「保存设置」才生效;模型的新增、
  保存、删除立即写入配置,「放弃更改」不回滚这些操作,两者各有中文反馈。
- 接口类型按现有 direct 实现说明三种行为,明确「自动」不探测对方服务。
- 文案收敛:⑤面板、标题栏网关状态与②余额展示不再出现 cmhub 字样;
  自定义网关下②显示「不计点数,费用由服务商收取」而非 0 点或旧余额,
  稳定取值 cmhub/direct 仅保留在 value 与 data-* 中。
- ⑥联动:自定义网关下停用「生成套图」与「AI 帮写」,同时保留
  「继续查询已提交图片」入口取回已扣点的默认网关任务,不新建、不重复计费。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 15:50:59 +08:00

2295 lines
128 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>蝦皮圈優化助手 - 工作台视觉提案 v1</title>
<style>
/* ============================================================
语义 Token 层
颜色基线取自 app/gui/widgets.py 的实际常量,便于原型与 Qt 实现对齐:
COLOR_SUCCESS #1a7f37 / COLOR_DANGER #cf222e / COLOR_INFO #0969da
COLOR_PENDING #9a6700 / COLOR_MUTED #6e7781 / COLOR_WARNING #bc4c00
间距遵循 4 epx 节奏;圆角对齐 BUTTON_RADIUS_PX = 4。
============================================================ */
:root {
color-scheme: light dark;
--font-ui: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
--font-mono: "Cascadia Mono", Consolas, "Microsoft YaHei UI", monospace;
--surface-base: #f6f8fa;
--surface-layer: #ffffff;
--surface-subtle: #f6f8fa;
--surface-overlay: #ffffff;
--surface-hover: #eef4ff;
--surface-pressed: #d8dee4;
--surface-window: #e6eaef;
--text-primary: #24292f;
--text-secondary: #57606a;
--text-disabled: #8c959f;
--accent-default: #0969da;
--accent-hover: #0860c4;
--accent-pressed: #0757ad;
--accent-subtle: #ddf0ff;
--status-success: #1a7f37;
--status-success-bg: #e6f6ea;
--status-caution: #bc4c00;
--status-caution-bg: #fff3e8;
--status-critical: #cf222e;
--status-critical-bg: #ffebe9;
--status-info: #0969da;
--status-info-bg: #ddf0ff;
--status-pending: #9a6700;
--status-pending-bg: #fff8dc;
--status-muted: #6e7781;
--status-muted-bg: #eef1f4;
--stroke-control: #d0d7de;
--stroke-strong: #8c959f;
--stroke-divider: #e4e8ec;
--focus-stroke: #0969da;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--radius-control: 4px;
--radius-overlay: 8px;
--motion-fast: 120ms;
--motion-normal: 200ms;
--ease: cubic-bezier(.2, 0, 0, 1);
--shadow-overlay: 0 12px 32px rgba(20, 30, 40, .18);
--shadow-flyout: 0 4px 14px rgba(20, 30, 40, .12);
}
:root[data-theme="dark"] {
--surface-base: #22272e;
--surface-layer: #2d333b;
--surface-subtle: #262c34;
--surface-overlay: #2d333b;
--surface-hover: #363d47;
--surface-pressed: #444c56;
--surface-window: #1c2128;
--text-primary: #e6edf3;
--text-secondary: #adbac7;
--text-disabled: #768390;
--accent-default: #539bf5;
--accent-hover: #6cb0ff;
--accent-pressed: #4285d6;
--accent-subtle: #1c3d5c;
--status-success: #57ab5a;
--status-success-bg: #1b3428;
--status-caution: #e0823d;
--status-caution-bg: #3a2515;
--status-critical: #e5534b;
--status-critical-bg: #3c1c1a;
--status-info: #539bf5;
--status-info-bg: #1c3d5c;
--status-pending: #c69026;
--status-pending-bg: #3a2f14;
--status-muted: #909dab;
--status-muted-bg: #343a42;
--stroke-control: #444c56;
--stroke-strong: #6b7681;
--stroke-divider: #373e47;
--focus-stroke: #539bf5;
--shadow-overlay: 0 12px 32px rgba(0, 0, 0, .5);
--shadow-flyout: 0 4px 14px rgba(0, 0, 0, .4);
}
/* 高对比度近似:加粗描边、去除弱背景、强制系统色可覆盖。
真实 Windows 高对比度必须在 Qt 端用系统调色板复验。 */
@media (prefers-contrast: more) {
:root {
--stroke-control: #24292f;
--stroke-strong: #24292f;
--stroke-divider: #24292f;
--text-secondary: #24292f;
--surface-hover: #ffffff;
--status-muted-bg: transparent;
--status-info-bg: transparent;
--status-success-bg: transparent;
--status-caution-bg: transparent;
--status-critical-bg: transparent;
--status-pending-bg: transparent;
}
:root[data-theme="dark"] {
--stroke-control: #ffffff;
--stroke-strong: #ffffff;
--stroke-divider: #ffffff;
--text-secondary: #ffffff;
}
.status-chip, button, .zone-prepare, .zone-feedback, .table-shell, .infobar { border-width: 2px !important; }
}
* { box-sizing: border-box; }
body {
min-width: 900px;
min-height: 100vh;
margin: 0;
background: var(--surface-window);
color: var(--text-primary);
font-family: var(--font-ui);
font-size: 14px;
line-height: 1.45;
}
button, input, select, textarea { font: inherit; color: inherit; }
button, select, input, textarea { outline: none; }
:is(button, select, input, textarea, [tabindex], a):focus-visible {
outline: 2px solid var(--focus-stroke);
outline-offset: 1px;
}
/* ---- 按钮:最小 32 epx 高,主要目标 ≥ 40 epx 宽 ---- */
button {
min-height: 32px;
min-width: 72px;
padding: var(--space-1) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
cursor: pointer;
white-space: nowrap;
transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
button:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--stroke-strong); }
button:active:not(:disabled) { background: var(--surface-pressed); }
button:disabled { color: var(--text-disabled); border-color: var(--stroke-divider); cursor: not-allowed; }
.btn-primary { background: var(--accent-default); border-color: var(--accent-default); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-primary:active:not(:disabled) { background: var(--accent-pressed); }
.btn-primary:disabled { background: var(--surface-subtle); border-color: var(--stroke-divider); color: var(--text-disabled); }
/* 警示样式:范围扩大 / 线上写入。非破坏性,不使用 danger 红。
与 T-663/664/665 定稿一致(COLOR_WARNING #bc4c00 描边)。 */
.btn-caution { border-color: var(--status-caution); color: var(--status-caution); background: var(--surface-layer); }
.btn-caution:hover:not(:disabled) { background: var(--status-caution-bg); border-color: var(--status-caution); }
/* 破坏性样式:仅用于不可恢复删除 */
.btn-danger { border-color: var(--status-critical); color: var(--status-critical); background: var(--surface-layer); }
.btn-danger:hover:not(:disabled) { background: var(--status-critical-bg); }
.btn-quiet { border-color: transparent; background: transparent; color: var(--text-secondary); min-width: 0; }
.btn-quiet:hover:not(:disabled) { background: var(--surface-hover); border-color: transparent; color: var(--text-primary); }
.btn-icon { min-width: 32px; width: 32px; padding: 0; }
label { color: var(--text-secondary); font-size: 12px; }
select, input[type="text"], input[type="password"], textarea {
width: 100%;
min-height: 32px;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
}
select:hover, input:hover, textarea:hover { border-color: var(--stroke-strong); }
textarea { min-height: 120px; resize: vertical; line-height: 1.55; font-family: var(--font-ui); }
input[type="checkbox"] { width: 16px; height: 16px; min-height: 0; margin: 0; accent-color: var(--accent-default); cursor: pointer; }
/* ============================================================
应用外壳
============================================================ */
.app-frame {
width: min(1560px, calc(100vw - 32px));
height: min(960px, calc(100vh - 32px));
min-height: 640px;
margin: 16px auto;
display: grid;
grid-template-rows: 44px 42px minmax(0, 1fr) 30px;
overflow: hidden;
border: 1px solid var(--stroke-control);
border-radius: var(--radius-overlay);
background: var(--surface-base);
box-shadow: var(--shadow-overlay);
}
.titlebar {
display: flex;
align-items: center;
gap: var(--space-3);
padding: 0 var(--space-2) 0 var(--space-4);
background: var(--surface-layer);
border-bottom: 1px solid var(--stroke-divider);
user-select: none;
}
.brand-mark {
width: 22px; height: 22px;
display: grid; place-items: center;
border-radius: var(--radius-control);
background: #e8543f; color: #fff;
font-weight: 700; font-size: 12px;
}
.app-name { font-weight: 600; }
.titlebar .push { margin-left: auto; }
.gateway-state { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-secondary); }
.gateway-state::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--status-success); }
.window-actions { display: flex; gap: 2px; margin-left: var(--space-2); }
.window-actions button { min-width: 40px; width: 40px; min-height: 32px; border: 0; background: transparent; color: var(--text-secondary); }
.window-actions button:hover { background: var(--surface-hover); }
.window-actions button.close:hover { background: #c42b1c; color: #fff; }
/* ---- 标签栏:静态任务标签 ---- */
.tabbar {
display: flex;
align-items: stretch;
gap: 2px;
padding: 0 var(--space-3);
background: var(--surface-subtle);
border-bottom: 1px solid var(--stroke-divider);
}
.tab-button {
position: relative;
min-height: 40px;
min-width: 0;
padding: 0 var(--space-3);
border: 0;
border-radius: 0;
background: transparent;
color: var(--text-secondary);
font-weight: 500;
}
.tab-button:hover { background: var(--surface-hover); border: 0; }
.tab-button[aria-selected="true"] { background: var(--surface-layer); color: var(--accent-default); font-weight: 600; }
.tab-button[aria-selected="true"]::after {
position: absolute; inset: auto var(--space-3) 0 var(--space-3);
height: 2px; background: var(--accent-default); content: "";
}
.tab-button .task-dot {
display: none;
width: 7px; height: 7px; margin-left: 6px;
border-radius: 50%; background: var(--status-info);
vertical-align: middle;
}
.tab-button.has-task .task-dot { display: inline-block; }
.page-stage { min-height: 0; overflow: hidden; }
.view { display: none; height: 100%; min-height: 0; }
.view.active { display: flex; flex-direction: column; }
/* ============================================================
统一页面骨架
页头 → 数据准备区 → 表格命令区 → 表格 → 状态反馈区 → 完成任务区
============================================================ */
.page-header {
display: flex; align-items: center; gap: var(--space-3);
min-height: 48px; padding: var(--space-2) var(--space-4);
background: var(--surface-layer);
border-bottom: 1px solid var(--stroke-divider);
}
.page-title { margin: 0; font-size: 16px; font-weight: 600; }
.page-scope { color: var(--text-secondary); font-size: 12px; }
.page-header .push { margin-left: auto; }
.page-body {
min-height: 0; flex: 1;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
gap: var(--space-3);
padding: var(--space-3) var(--space-4) var(--space-4);
overflow: hidden;
}
.zone-prepare {
display: flex; align-items: center; flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
min-height: 44px;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
font-size: 13px;
}
.zone-prepare .push { margin-left: auto; }
.zone-prepare .filename { font-family: var(--font-mono); font-size: 12px; }
.inline-field { display: flex; align-items: center; gap: var(--space-2); }
.inline-field select { width: auto; min-width: 132px; }
.zone-tablecmd { display: flex; align-items: end; flex-wrap: wrap; gap: var(--space-2); }
.field { display: grid; gap: var(--space-1); min-width: 0; }
.field.w-sm { width: 128px; }
.field.w-md { width: 156px; }
.field.w-lg { width: 200px; }
.zone-tablecmd .result-count { margin-left: auto; color: var(--text-secondary); font-size: 12px; align-self: center; }
.table-shell {
min-height: 120px;
overflow: auto;
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
}
table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
th, td { padding: var(--space-2) var(--space-2); text-align: left; }
th {
position: sticky; top: 0; z-index: 1;
background: var(--surface-subtle);
border-bottom: 1px solid var(--stroke-control);
color: var(--text-secondary);
font-size: 12px; font-weight: 600;
white-space: nowrap;
}
th.sortable { cursor: pointer; user-select: none; }
th.sortable:hover { background: var(--surface-hover); color: var(--text-primary); }
th .sort-mark { opacity: 0; margin-left: 4px; }
th[aria-sort="ascending"] .sort-mark, th[aria-sort="descending"] .sort-mark { opacity: 1; }
th[aria-sort="descending"] .sort-mark { display: inline-block; transform: rotate(180deg); }
td {
height: 40px;
border-bottom: 1px solid var(--stroke-divider);
font-size: 13px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.col-check { width: 40px; }
.col-check td, th.col-check { text-align: center; padding-left: 0; padding-right: 0; }
tbody tr:hover { background: var(--surface-hover); }
/* 当前行、选择、焦点三种状态视觉可区分,且不只依赖颜色 */
tbody tr[aria-selected="true"] { background: var(--accent-subtle); }
tbody tr.current td:first-child { box-shadow: inset 3px 0 0 var(--accent-default); }
tbody tr:focus-visible { outline: 2px solid var(--focus-stroke); outline-offset: -2px; }
tbody tr:last-child td { border-bottom: 0; }
.muted { color: var(--text-secondary); }
.nowrap-id { font-family: var(--font-mono); font-size: 12px; }
.table-empty {
display: none;
padding: var(--space-6) var(--space-4);
text-align: center;
color: var(--text-secondary);
}
.table-empty strong { display: block; margin-bottom: var(--space-1); color: var(--text-primary); font-size: 14px; }
.table-shell.is-empty table { display: none; }
.table-shell.is-empty .table-empty { display: block; }
/* ---- 状态反馈区:选择工具栏 / 进度 / 信息条,插入位置稳定 ---- */
.zone-feedback { display: grid; gap: var(--space-2); }
.selection-bar {
display: none;
align-items: center; gap: var(--space-3);
min-height: 40px; padding: var(--space-1) var(--space-3);
border: 1px solid var(--accent-default);
border-radius: var(--radius-control);
background: var(--accent-subtle);
font-size: 13px;
}
.selection-bar.show { display: flex; }
.selection-bar .push { margin-left: auto; }
.progress-deck { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
.progress-row {
display: grid; grid-template-columns: auto minmax(80px, 1fr) auto;
align-items: center; gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
font-size: 12px;
}
.progress-track { height: 6px; overflow: hidden; border-radius: 3px; background: var(--surface-pressed); }
.progress-value { height: 100%; width: 0; border-radius: inherit; background: var(--accent-default); transition: width var(--motion-normal) var(--ease); }
.progress-value.done { background: var(--status-success); }
/* 不确定进度:总量未知时使用 */
.progress-track.indeterminate .progress-value {
width: 35% !important;
animation: indeterminate 1.4s ease-in-out infinite;
}
@keyframes indeterminate { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
.progress-meta { color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* ---- 信息条:页面级可恢复问题 / 部分成功,持久直到解决或关闭 ---- */
.infobar {
display: none;
align-items: flex-start; gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-left-width: 4px;
border-radius: var(--radius-control);
background: var(--surface-layer);
font-size: 13px;
}
.infobar.show { display: flex; }
.infobar .body { flex: 1; min-width: 0; }
.infobar .body strong { display: block; }
.infobar .body .detail { color: var(--text-secondary); font-size: 12px; }
.infobar .actions { display: flex; gap: var(--space-2); flex-shrink: 0; }
.infobar.caution { border-left-color: var(--status-caution); background: var(--status-caution-bg); }
.infobar.critical { border-left-color: var(--status-critical); background: var(--status-critical-bg); }
.infobar.info { border-left-color: var(--status-info); background: var(--status-info-bg); }
/* ---- 完成任务区 ---- */
.zone-complete {
display: flex; align-items: center; gap: var(--space-3);
flex-wrap: wrap;
min-height: 48px;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
}
.zone-complete .scope-note { color: var(--text-secondary); font-size: 12px; }
.zone-complete .push { margin-left: auto; }
.zone-complete .actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* ---- 状态 chip:颜色 + 文本双通道 ---- */
.status-chip {
display: inline-flex; align-items: center; gap: 4px;
min-height: 22px; padding: 1px var(--space-2);
border: 1px solid transparent;
border-radius: 11px;
font-size: 12px; white-space: nowrap;
}
.chip-success { color: var(--status-success); background: var(--status-success-bg); border-color: currentColor; }
.chip-caution { color: var(--status-caution); background: var(--status-caution-bg); border-color: currentColor; }
.chip-critical { color: var(--status-critical); background: var(--status-critical-bg); border-color: currentColor; }
.chip-info { color: var(--status-info); background: var(--status-info-bg); border-color: currentColor; }
.chip-pending { color: var(--status-pending); background: var(--status-pending-bg); border-color: currentColor; }
.chip-muted { color: var(--status-muted); background: var(--status-muted-bg); border-color: currentColor; }
/* ============================================================
② AI生成:提示词侧面板(前置配置)
============================================================ */
.generate-layout { min-height: 0; flex: 1; display: grid; grid-template-columns: minmax(280px, .68fr) minmax(560px, 2fr); overflow: hidden; }
.prompt-pane {
display: flex; flex-direction: column; gap: var(--space-3);
min-height: 0; padding: var(--space-3) var(--space-4);
overflow: auto;
background: var(--surface-subtle);
border-right: 1px solid var(--stroke-divider);
}
.prompt-pane .page-body { padding: 0; }
.editor-group { display: grid; gap: var(--space-2); }
.group-head { display: flex; align-items: center; gap: var(--space-2); }
.group-head strong { font-size: 13px; }
.group-head .push { margin-left: auto; }
.editor-toolbar { display: flex; gap: var(--space-2); }
.editor-toolbar select { flex: 1; min-width: 0; }
.generate-main { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; gap: var(--space-3); padding: var(--space-3) var(--space-4) var(--space-4); overflow: hidden; }
.run-log {
max-height: 72px; overflow: auto;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-divider);
border-radius: var(--radius-control);
background: var(--surface-subtle);
color: var(--text-secondary);
font-family: var(--font-mono); font-size: 12px; line-height: 1.6;
}
/* ============================================================
列表/详情页(账号管理、商品套图)
============================================================ */
.split-layout { min-height: 0; flex: 1; display: grid; grid-template-columns: minmax(230px, .7fr) minmax(420px, 2fr); gap: var(--space-3); padding: var(--space-3) var(--space-4) var(--space-4); overflow: hidden; }
.side-list { min-height: 0; overflow: auto; padding: var(--space-1); border: 1px solid var(--stroke-control); border-radius: var(--radius-control); background: var(--surface-layer); }
.list-item {
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: var(--space-2);
width: 100%; min-height: 48px; padding: var(--space-2) var(--space-2);
border: 0; border-bottom: 1px solid var(--stroke-divider); border-radius: 0;
background: transparent; text-align: left;
}
.list-item:last-child { border-bottom: 0; }
.list-item[aria-current="true"] { background: var(--accent-subtle); box-shadow: inset 3px 0 0 var(--accent-default); }
.list-main { min-width: 0; }
.list-title { font-weight: 600; font-size: 13px; }
.list-meta { overflow: hidden; color: var(--text-secondary); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.detail-pane { min-height: 0; overflow: auto; border: 1px solid var(--stroke-control); border-radius: var(--radius-control); background: var(--surface-layer); }
.form-section { padding: var(--space-4); border-bottom: 1px solid var(--stroke-divider); }
.form-section:last-child { border-bottom: 0; }
.form-section h2 { margin: 0 0 var(--space-3); font-size: 13px; font-weight: 600; }
.section-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-3); }
.section-head h2 { margin: 0; }
.section-head .push { margin-left: auto; }
/* ---- 分段选择器:表达“当前来源”这一状态,不是动作按钮 ----
Qt 端对应 QButtonGroup(setExclusive) + setCheckable 的两个按钮,
或两个 QRadioButton;不得退化为普通 QPushButton。 */
.segmented { display: inline-flex; border: 1px solid var(--stroke-control); border-radius: var(--radius-control); overflow: hidden; }
.segmented label {
display: flex; align-items: center;
min-height: 32px; padding: 0 var(--space-3);
border-right: 1px solid var(--stroke-control);
color: var(--text-primary); font-size: 13px; cursor: pointer;
transition: background var(--motion-fast) var(--ease);
}
.segmented label:last-of-type { border-right: 0; }
.segmented label:hover { background: var(--surface-hover); }
.segmented label.is-checked { background: var(--accent-default); color: #fff; font-weight: 600; }
.segmented label.is-focused { outline: 2px solid var(--focus-stroke); outline-offset: -2px; }
.segmented input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.gateway-panel[hidden] { display: none; }
.gateway-note { margin: 0 0 var(--space-3); }
.model-layout { display: grid; grid-template-columns: minmax(170px, .55fr) minmax(0, 1.45fr); gap: var(--space-3); align-items: start; }
.model-list { max-height: 232px; overflow: auto; padding: var(--space-1); border: 1px solid var(--stroke-control); border-radius: var(--radius-control); background: var(--surface-layer); }
.model-item {
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: var(--space-2);
width: 100%; min-height: 40px; padding: var(--space-1) var(--space-2);
border: 0; border-radius: var(--radius-control); background: transparent; text-align: left;
}
.model-item[aria-current="true"] { background: var(--accent-subtle); box-shadow: inset 3px 0 0 var(--accent-default); }
.model-item .name { display: block; font-size: 13px; font-weight: 600; }
.model-item .meta { display: block; overflow: hidden; color: var(--text-secondary); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.test-result { display: inline-flex; align-items: center; font-size: 12px; color: var(--text-secondary); }
@media (max-width: 1000px) { .model-layout { grid-template-columns: minmax(0, 1fr); } .model-list { max-height: 160px; } }
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); }
.form-grid .span-2 { grid-column: span 2; }
.form-grid .span-3 { grid-column: 1 / -1; }
.field-hint { color: var(--text-secondary); font-size: 11px; }
.button-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.button-row .separator { width: 1px; align-self: stretch; margin: 0 var(--space-2); background: var(--stroke-divider); }
/* ---- 商品套图 ---- */
.suite-main { min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: var(--space-3); }
.gallery-scroll { min-height: 0; overflow: auto; display: grid; gap: var(--space-3); align-content: start; }
.gallery-section { padding: var(--space-3); border: 1px solid var(--stroke-control); border-radius: var(--radius-control); background: var(--surface-layer); }
.gallery-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.gallery-head strong { font-size: 13px; }
.gallery-head .push { margin-left: auto; }
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--space-2); }
.photo-thumb {
position: relative; min-height: 96px; padding: 0;
border: 1px solid var(--stroke-control); border-radius: var(--radius-control);
background: var(--surface-subtle); min-width: 0;
}
.photo-thumb::before { position: absolute; inset: 18px 20px 14px; border: 2px solid var(--stroke-control); border-radius: 45% 45% 18% 18%; content: ""; }
.photo-thumb[aria-pressed="true"] { border: 2px solid var(--accent-default); }
.photo-thumb .tag { position: absolute; right: 4px; bottom: 4px; padding: 0 5px; border-radius: 3px; background: var(--accent-default); color: #fff; font-size: 11px; }
.photo-thumb .tag { display: none; }
.photo-thumb[aria-pressed="true"] .tag { display: block; }
/* ============================================================
状态栏 / 反馈层
============================================================ */
.statusbar {
display: flex; align-items: center; gap: var(--space-3);
padding: 0 var(--space-4);
border-top: 1px solid var(--stroke-divider);
background: var(--surface-layer);
color: var(--text-secondary); font-size: 12px;
}
.statusbar .push { margin-left: auto; }
.statusbar .state { display: inline-flex; align-items: center; gap: 6px; }
.statusbar .state::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--status-success); }
.statusbar .state.busy::before { background: var(--status-info); }
.statusbar .btn-quiet { min-height: 24px; padding: 0 var(--space-2); font-size: 12px; }
/* Toast:仅用于无关键信息、错过也不阻塞任务的短反馈 */
.toast {
position: fixed; right: 24px; bottom: 24px; z-index: 40;
display: none; max-width: 360px;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-left: 4px solid var(--status-success);
border-radius: var(--radius-control);
background: var(--surface-overlay);
box-shadow: var(--shadow-flyout);
font-size: 13px;
}
.toast.show { display: block; }
/* ---- 对话框 ---- */
dialog {
width: min(560px, calc(100vw - 40px));
padding: 0;
border: 1px solid var(--stroke-control);
border-radius: var(--radius-overlay);
background: var(--surface-overlay);
color: var(--text-primary);
box-shadow: var(--shadow-overlay);
}
dialog::backdrop { background: rgba(16, 24, 32, .45); }
dialog.wide { width: min(680px, calc(100vw - 40px)); }
.dialog-body { padding: var(--space-5); }
.dialog-title { margin: 0 0 var(--space-2); font-size: 16px; font-weight: 600; }
.dialog-copy { margin: 0; color: var(--text-secondary); font-size: 13px; }
.dialog-actions {
display: flex; justify-content: flex-end; gap: var(--space-2);
padding: var(--space-3) var(--space-5);
border-top: 1px solid var(--stroke-divider);
background: var(--surface-subtle);
}
.dialog-actions .push { margin-right: auto; }
/* 范围选择对话框:纵向全宽按钮,最小内容宽度 520px(T-663 定稿) */
dialog.scope-dialog { width: min(560px, calc(100vw - 40px)); min-width: 520px; }
.scope-options { display: grid; gap: var(--space-2); margin-top: var(--space-4); }
.scope-option {
display: grid; gap: 2px;
width: 100%; min-height: 60px;
padding: var(--space-2) var(--space-3);
text-align: left; white-space: normal;
}
.scope-option strong { font-size: 14px; font-weight: 600; }
.scope-option span { color: var(--text-secondary); font-size: 12px; }
.scope-option.btn-primary span { color: rgba(255, 255, 255, .88); }
.scope-option.btn-caution span { color: var(--status-caution); opacity: .9; }
.breakdown { display: grid; gap: var(--space-1); margin-top: var(--space-3); font-size: 13px; }
.breakdown-row { display: flex; align-items: center; gap: var(--space-2); }
.breakdown-row .count { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; }
.breakdown-row .sample { color: var(--text-secondary); font-size: 12px; font-family: var(--font-mono); }
.assumption-list { margin: var(--space-3) 0 0; padding-left: var(--space-5); color: var(--text-secondary); font-size: 13px; }
.assumption-list li { margin-bottom: var(--space-1); }
.assumption-list strong { color: var(--text-primary); }
/* ============================================================
响应式:中等宽度收起提示词侧栏与次要筛选
============================================================ */
@media (max-width: 1180px) {
.app-frame { width: calc(100vw - 16px); height: calc(100vh - 16px); margin: 8px auto; }
.generate-layout { grid-template-columns: minmax(260px, .8fr) minmax(0, 2fr); }
.form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.progress-deck { grid-template-columns: 1fr; }
}
@media (max-width: 1000px) {
.generate-layout { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
.prompt-pane { border-right: 0; border-bottom: 1px solid var(--stroke-divider); max-height: 210px; }
.split-layout { grid-template-columns: minmax(0, 1fr); grid-template-rows: 160px minmax(0, 1fr); }
.form-grid { grid-template-columns: minmax(0, 1fr); }
.form-grid .span-2, .form-grid .span-3 { grid-column: auto; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
}
.visually-hidden {
position: absolute; width: 1px; height: 1px;
margin: -1px; padding: 0; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
</style>
</head>
<body>
<div class="app-frame">
<!-- ============ 应用外壳 ============ -->
<header class="titlebar">
<span class="brand-mark" aria-hidden="true">虾</span>
<strong class="app-name">蝦皮圈優化助手</strong>
<span class="push"></span>
<span class="gateway-state" id="titlebar-gateway">默认网关 已连接</span>
<button class="btn-quiet" type="button" id="theme-toggle" aria-pressed="false">深色主题</button>
<div class="window-actions" aria-label="窗口操作(原型示意,非原生标题栏)">
<button type="button" aria-label="最小化">&#8722;</button>
<button type="button" aria-label="最大化">&#9633;</button>
<button class="close" type="button" aria-label="关闭">&#10005;</button>
</div>
</header>
<nav class="tabbar" role="tablist" aria-label="主工作流">
<button class="tab-button" role="tab" id="tab-collect" aria-selected="false" aria-controls="view-collect" tabindex="-1" data-view="collect">① 导入采集<span class="task-dot" aria-hidden="true"></span></button>
<button class="tab-button" role="tab" id="tab-generate" aria-selected="true" aria-controls="view-generate" tabindex="0" data-view="generate">② AI生成<span class="task-dot" aria-hidden="true"></span></button>
<button class="tab-button" role="tab" id="tab-apply" aria-selected="false" aria-controls="view-apply" tabindex="-1" data-view="apply">③ 更新蝦皮<span class="task-dot" aria-hidden="true"></span></button>
<button class="tab-button" role="tab" id="tab-suite" aria-selected="false" aria-controls="view-suite" tabindex="-1" data-view="suite">商品套图<span class="task-dot" aria-hidden="true"></span></button>
<button class="tab-button" role="tab" id="tab-accounts" aria-selected="false" aria-controls="view-accounts" tabindex="-1" data-view="accounts">账号管理</button>
<button class="tab-button" role="tab" id="tab-settings" aria-selected="false" aria-controls="view-settings" tabindex="-1" data-view="settings">设置</button>
</nav>
<main class="page-stage">
<!-- ============================================================
① 导入采集
============================================================ -->
<section class="view" id="view-collect" role="tabpanel" aria-labelledby="tab-collect" tabindex="-1">
<header class="page-header">
<h1 class="page-title">导入采集</h1>
<span class="page-scope">读取商品旧标题与原封面,不修改线上商品</span>
<span class="push"></span>
<span class="status-chip chip-info">批次 2026-07-20 14:32</span>
</header>
<div class="page-body">
<!-- 数据准备区:数据来源与前置配置 -->
<div class="zone-prepare">
<span class="filename">7月改版清单.xlsx</span>
<span class="muted">解析 128 行</span>
<span class="status-chip chip-success">有效 125</span>
<span class="status-chip chip-critical">无效 3</span>
<button class="btn-quiet" type="button" data-open="invalid-rows-dialog">查看无效行</button>
<span class="push"></span>
<button type="button" data-toast="示意:已打开文件选择窗口">选择 Excel 文件…</button>
<div class="inline-field">
<label class="visually-hidden" for="collect-import-mode">再次导入方式</label>
<select id="collect-import-mode">
<option>追加导入到当前批次</option>
<option>替换当前批次数据</option>
</select>
</div>
<button type="button" data-confirm-import>导入</button>
</div>
<!-- 表格命令区 -->
<div class="zone-tablecmd" data-table-scope="collect">
<div class="field w-md"><label for="collect-batch">导入批次</label><select id="collect-batch"><option>2026-07-20 14:32</option><option>2026-07-18 09:10</option></select></div>
<div class="field w-md"><label for="collect-shop">店铺</label><select id="collect-shop"><option>全部店铺</option><option>台北生活店</option><option>质感好物店</option></select></div>
<div class="field w-lg"><label for="collect-search">商品 ID</label><input type="text" id="collect-search" data-search placeholder="输入后即时筛选"></div>
<div class="field w-md"><label for="collect-stage">处理状态</label><select id="collect-stage" data-filter-stage><option value="">全部状态</option><option value="待采集">待采集</option><option value="已采集">已采集</option><option value="略过">略过</option><option value="失败">失败</option></select></div>
<button class="btn-quiet" type="button" data-clear-filter>清除筛选</button>
<button type="button" data-refresh title="F5">刷新列表</button>
<span class="result-count" data-result-count aria-live="polite"></span>
</div>
<!-- 主工作区 -->
<div class="table-shell">
<table data-table="collect" aria-label="导入采集任务列表">
<colgroup><col class="col-check"><col style="width:17%"><col style="width:15%"><col style="width:17%"><col style="width:13%"><col></colgroup>
<thead>
<tr>
<th class="col-check"><input type="checkbox" data-select-all aria-label="选择当前筛选结果中的全部任务"></th>
<th class="sortable" aria-sort="none" data-sort-key="shop">店铺<span class="sort-mark" aria-hidden="true">▲</span></th>
<th>别名</th>
<th class="sortable" aria-sort="none" data-sort-key="id">商品 ID<span class="sort-mark" aria-hidden="true">▲</span></th>
<th class="sortable" aria-sort="none" data-sort-key="stage">阶段<span class="sort-mark" aria-hidden="true">▲</span></th>
<th>采集结果</th>
</tr>
</thead>
<tbody>
<tr data-id="51513423876" data-stage="待采集"><td><input type="checkbox" data-row-check aria-label="选择商品 51513423876"></td><td>台北生活店</td><td>taipei_life</td><td class="nowrap-id">51513423876</td><td><span class="status-chip chip-muted">待采集</span></td><td class="muted">等待本轮采集</td></tr>
<tr data-id="51813423856" data-stage="已采集"><td><input type="checkbox" data-row-check aria-label="选择商品 51813423856"></td><td>台北生活店</td><td>taipei_life</td><td class="nowrap-id">51813423856</td><td><span class="status-chip chip-success">已采集</span></td><td>旧标题、原封面已保存</td></tr>
<tr data-id="48663456321" data-stage="略过"><td><input type="checkbox" data-row-check aria-label="选择商品 48663456321"></td><td>质感好物店</td><td>quality_goods</td><td class="nowrap-id">48663456321</td><td><span class="status-chip chip-caution">略过</span></td><td class="muted" title="审核中,按本轮采集范围略过">审核中,按本轮采集范围略过</td></tr>
<tr data-id="52313423890" data-stage="失败"><td><input type="checkbox" data-row-check aria-label="选择商品 52313423890"></td><td>台北生活店</td><td>taipei_life</td><td class="nowrap-id">52313423890</td><td><span class="status-chip chip-critical">失败</span></td><td class="muted" title="账号未登录,请在账号管理确认登录状态">账号未登录,请在账号管理确认登录</td></tr>
<tr data-id="49913423712" data-stage="已采集"><td><input type="checkbox" data-row-check aria-label="选择商品 49913423712"></td><td>质感好物店</td><td>quality_goods</td><td class="nowrap-id">49913423712</td><td><span class="status-chip chip-success">已采集</span></td><td>旧标题、原封面已保存</td></tr>
</tbody>
</table>
<div class="table-empty">
<strong>当前筛选没有匹配的任务</strong>
<span>调整筛选条件,或清除筛选查看全部 5 条任务。</span>
</div>
</div>
<!-- 状态反馈区 -->
<div class="zone-feedback">
<div class="selection-bar" data-selection-bar>
<span data-selection-text>已选 0 项</span>
<span class="push"></span>
<button type="button" data-toast="示意:仅对所选任务重新采集">仅重新采集所选</button>
<button class="btn-quiet" type="button" data-clear-selection>清除选择</button>
</div>
<div class="infobar caution" id="collect-result-bar">
<div class="body">
<strong>上一轮采集部分完成:成功 122,失败 2,按范围略过 1</strong>
<span class="detail">失败原因集中在账号未登录(2 条)。已成功的采集结果已保留,重试只会处理失败项。</span>
</div>
<div class="actions">
<button type="button" data-toast="示意:已按失败原因筛选任务列表">查看失败项</button>
<button type="button" data-toast="示意:仅重试 2 条失败任务">重试失败项</button>
<button class="btn-quiet btn-icon" type="button" data-dismiss="collect-result-bar" aria-label="关闭提示">&#10005;</button>
</div>
</div>
</div>
<!-- 完成任务区 -->
<div class="zone-complete">
<span class="scope-note" data-scope-note>本轮范围:当前筛选 5 条 · 采集只读取商品页面,不修改线上商品</span>
<span class="push"></span>
<div class="actions">
<button type="button" data-toast="示意:已把旧标题与原封面写回 Excel">回写旧数据到 Excel</button>
<button type="button" data-stop="collect" disabled>停止</button>
<button class="btn-primary" type="button" data-open="collect-scope-dialog">开始采集</button>
</div>
</div>
</div>
</section>
<!-- ============================================================
② AI生成
============================================================ -->
<section class="view active" id="view-generate" role="tabpanel" aria-labelledby="tab-generate" tabindex="-1">
<header class="page-header">
<h1 class="page-title">AI生成</h1>
<span class="page-scope" id="generate-scope-text">通过默认网关生成新标题与新封面,会消耗点数</span>
<span class="push"></span>
<!-- 余额文案随⑤生成网关来源变化(T-677 第 5 节:不再出现 cmhub 字样) -->
<span class="status-chip chip-success" id="generate-balance">默认网关余额:1,280 点</span>
<span class="status-chip chip-info">批次 2026-07-20 14:32</span>
<button type="button" data-toast="已打开当前批次的图片文件夹">打开图片文件夹</button>
</header>
<div class="generate-layout">
<!-- 前置配置:提示词侧面板 -->
<aside class="prompt-pane" aria-label="提示词设置">
<div class="editor-group">
<div class="group-head">
<strong>标题提示词</strong>
<span class="push"></span>
<span class="status-chip chip-muted">支持变量</span>
</div>
<div class="editor-toolbar">
<label class="visually-hidden" for="title-template">标题模板</label>
<select id="title-template"><option>夏季清爽标题</option><option>高转化标题</option></select>
<button class="btn-quiet" type="button" data-toast="示意:已新建标题模板">新建</button>
</div>
<label class="visually-hidden" for="title-prompt">标题提示词内容</label>
<textarea id="title-prompt">你是蝦皮台灣站电商标题助手。根据商品旧标题,输出清晰、自然的繁体中文新标题,不超过 60 字。
旧标题:{旧标题}</textarea>
<div class="button-row">
<button class="btn-quiet" type="button" data-toast="已插入变量 {旧标题}">插入变量</button>
<span class="push"></span>
<button type="button" data-toast="标题提示词已保存">保存</button>
</div>
</div>
<div class="editor-group">
<div class="group-head">
<strong>封面提示词</strong>
<span class="push"></span>
<span class="status-chip chip-muted">图生图</span>
</div>
<div class="editor-toolbar">
<label class="visually-hidden" for="cover-template">封面模板</label>
<select id="cover-template"><option>虾皮商品主图</option><option>细节卖点主图</option></select>
<button class="btn-quiet" type="button" data-toast="示意:已新建封面模板">新建</button>
</div>
<label class="visually-hidden" for="cover-prompt">封面提示词内容</label>
<textarea id="cover-prompt">为蝦皮商品生成清晰主图,保留商品颜色、材质和关键细节,不添加文字水印。
标题参考:{新标题}</textarea>
<div class="button-row">
<button class="btn-quiet" type="button" data-toast="已插入变量 {新标题}">插入变量</button>
<span class="push"></span>
<button type="button" data-toast="封面提示词已保存">保存</button>
</div>
</div>
</aside>
<section class="generate-main" aria-label="AI生成任务">
<div class="zone-tablecmd" data-table-scope="generate">
<div class="field w-md"><label for="generate-shop">店铺</label><select id="generate-shop"><option>全部店铺</option><option>台北生活店</option></select></div>
<div class="field w-lg"><label for="generate-search">商品 ID</label><input type="text" id="generate-search" data-search placeholder="输入后即时筛选"></div>
<div class="field w-md"><label for="generate-stage">生成状态</label><select id="generate-stage" data-filter-stage><option value="">全部状态</option><option value="待生成">待生成</option><option value="已生成">已生成</option><option value="失败">失败</option></select></div>
<button class="btn-quiet" type="button" data-clear-filter>清除筛选</button>
<button type="button" data-refresh title="F5">刷新列表</button>
<span class="result-count" data-result-count aria-live="polite"></span>
</div>
<div class="table-shell">
<table data-table="generate" aria-label="AI生成任务列表">
<colgroup><col class="col-check"><col style="width:14%"><col style="width:15%"><col style="width:23%"><col style="width:24%"><col></colgroup>
<thead>
<tr>
<th class="col-check"><input type="checkbox" data-select-all aria-label="选择当前筛选结果中的全部任务"></th>
<th class="sortable" aria-sort="none" data-sort-key="shop">店铺<span class="sort-mark" aria-hidden="true">▲</span></th>
<th class="sortable" aria-sort="none" data-sort-key="id">商品 ID<span class="sort-mark" aria-hidden="true">▲</span></th>
<th>旧标题</th>
<th>新标题</th>
<th class="sortable" aria-sort="none" data-sort-key="stage">生成状态<span class="sort-mark" aria-hidden="true">▲</span></th>
</tr>
</thead>
<tbody>
<tr data-id="51513423876" data-stage="已生成"><td><input type="checkbox" data-row-check aria-label="选择商品 51513423876"></td><td>台北生活店</td><td class="nowrap-id">51513423876</td><td title="簡約隨行保溫杯">簡約隨行保溫杯</td><td title="大容量隨行保溫杯|輕巧防漏 316 不鏽鋼">大容量隨行保溫杯|輕巧防漏 316 不鏽鋼</td><td><span class="status-chip chip-success">标题、图片已生成</span></td></tr>
<tr data-id="51813423856" data-stage="待生成"><td><input type="checkbox" data-row-check aria-label="选择商品 51813423856"></td><td>台北生活店</td><td class="nowrap-id">51813423856</td><td title="棉麻收納籃">棉麻收納籃</td><td class="muted">等待生成</td><td><span class="status-chip chip-muted">待生成</span></td></tr>
<tr data-id="48663456321" data-stage="失败"><td><input type="checkbox" data-row-check aria-label="选择商品 48663456321"></td><td>质感好物店</td><td class="nowrap-id">48663456321</td><td title="居家小夜燈">居家小夜燈</td><td title="暖光感應小夜燈|即插即用">暖光感應小夜燈|即插即用</td><td><span class="status-chip chip-critical" title="图片下载超时,标题已保存">图片失败,标题已保存</span></td></tr>
<tr data-id="52313423890" data-stage="待生成"><td><input type="checkbox" data-row-check aria-label="选择商品 52313423890"></td><td>台北生活店</td><td class="nowrap-id">52313423890</td><td title="可折疊購物袋">可折疊購物袋</td><td class="muted">等待生成</td><td><span class="status-chip chip-muted">待生成</span></td></tr>
<tr data-id="49913423712" data-stage="已生成"><td><input type="checkbox" data-row-check aria-label="选择商品 49913423712"></td><td>质感好物店</td><td class="nowrap-id">49913423712</td><td title="厚磅純棉浴巾">厚磅純棉浴巾</td><td title="飯店級厚磅純棉浴巾|吸水快乾">飯店級厚磅純棉浴巾|吸水快乾</td><td><span class="status-chip chip-success">标题、图片已生成</span></td></tr>
</tbody>
</table>
<div class="table-empty">
<strong>当前筛选没有匹配的任务</strong>
<span>调整筛选条件,或清除筛选查看全部 5 条任务。</span>
</div>
</div>
<div class="zone-feedback">
<div class="selection-bar" data-selection-bar>
<span data-selection-text>已选 0 项</span>
<span class="push"></span>
<button type="button" data-toast="示意:仅对所选任务补生成">仅生成所选</button>
<button class="btn-caution" type="button" data-open="reset-results-dialog">重置所选生成结果</button>
<button class="btn-quiet" type="button" data-clear-selection>清除选择</button>
</div>
<div class="progress-deck" id="generate-progress" hidden>
<div class="progress-row">
<strong data-progress-label="title">标题 0/5</strong>
<div class="progress-track"><div class="progress-value" data-progress-bar="title"></div></div>
<span class="progress-meta" data-progress-meta="title">等待开始</span>
</div>
<div class="progress-row">
<strong data-progress-label="image">图片 0/5</strong>
<div class="progress-track"><div class="progress-value" data-progress-bar="image"></div></div>
<span class="progress-meta" data-progress-meta="image">等待开始</span>
</div>
</div>
<div class="infobar" id="generate-result-bar">
<div class="body">
<strong data-result-title></strong>
<span class="detail" data-result-detail></span>
</div>
<div class="actions">
<button type="button" data-toast="示意:已按失败原因筛选任务列表">查看失败项</button>
<button type="button" data-toast="示意:仅重试失败项,已完成结果保留">重试失败项</button>
<button class="btn-quiet btn-icon" type="button" data-dismiss="generate-result-bar" aria-label="关闭提示">&#10005;</button>
</div>
</div>
<div class="run-log" aria-live="polite" id="generate-log">14:36:12 [就绪] 当前筛选 5 条 · 标题并发 4,图片并发 5。</div>
</div>
<div class="zone-complete">
<div class="inline-field">
<label for="generate-mode">生成内容</label>
<select id="generate-mode"><option>标题和封面</option><option>只生成标题</option><option>只生成封面</option></select>
</div>
<span class="scope-note" data-scope-note>本轮范围:当前筛选 5 条</span>
<span class="push"></span>
<div class="actions">
<button type="button" data-stop="generate" disabled>停止</button>
<button class="btn-primary" type="button" data-open="generate-scope-dialog" id="start-generate">开始生成</button>
</div>
</div>
</section>
</div>
</section>
<!-- ============================================================
③ 更新蝦皮
============================================================ -->
<section class="view" id="view-apply" role="tabpanel" aria-labelledby="tab-apply" tabindex="-1">
<header class="page-header">
<h1 class="page-title">更新蝦皮</h1>
<span class="page-scope">把已生成的标题与封面提交到蝦皮线上商品</span>
<span class="push"></span>
<span class="status-chip chip-caution">线上写入操作</span>
</header>
<div class="page-body">
<div class="zone-prepare">
<div class="inline-field">
<label for="apply-mode">更新内容</label>
<select id="apply-mode"><option>标题和封面</option><option>只更新标题</option><option>只更新封面</option></select>
</div>
<span class="muted">将按店铺顺序执行,最多 2 个账号并行</span>
<span class="push"></span>
<span class="muted">开始前会检测账号 Chrome 与登录状态</span>
</div>
<div class="zone-tablecmd" data-table-scope="apply">
<div class="field w-md"><label for="apply-batch">导入批次</label><select id="apply-batch"><option>2026-07-20 14:32</option></select></div>
<div class="field w-md"><label for="apply-shop">店铺</label><select id="apply-shop"><option>全部店铺</option><option>台北生活店</option></select></div>
<div class="field w-lg"><label for="apply-search">商品 ID</label><input type="text" id="apply-search" data-search placeholder="输入后即时筛选"></div>
<div class="field w-md"><label for="apply-stage">更新状态</label><select id="apply-stage" data-filter-stage><option value="">全部状态</option><option value="待更新">待更新</option><option value="已更新">已更新</option><option value="失败">失败</option></select></div>
<button class="btn-quiet" type="button" data-clear-filter>清除筛选</button>
<button type="button" data-refresh title="F5">刷新列表</button>
<span class="result-count" data-result-count aria-live="polite"></span>
</div>
<div class="table-shell">
<table data-table="apply" aria-label="更新蝦皮任务列表">
<colgroup><col class="col-check"><col style="width:16%"><col style="width:16%"><col style="width:28%"><col style="width:18%"><col></colgroup>
<thead>
<tr>
<th class="col-check"><input type="checkbox" data-select-all aria-label="选择当前筛选结果中的全部任务"></th>
<th class="sortable" aria-sort="none" data-sort-key="shop">店铺<span class="sort-mark" aria-hidden="true">▲</span></th>
<th class="sortable" aria-sort="none" data-sort-key="id">商品 ID<span class="sort-mark" aria-hidden="true">▲</span></th>
<th>新标题</th>
<th>新封面</th>
<th class="sortable" aria-sort="none" data-sort-key="stage">更新状态<span class="sort-mark" aria-hidden="true">▲</span></th>
</tr>
</thead>
<tbody>
<tr data-id="51513423876" data-stage="待更新"><td><input type="checkbox" data-row-check aria-label="选择商品 51513423876"></td><td>台北生活店</td><td class="nowrap-id">51513423876</td><td title="大容量隨行保溫杯|輕巧防漏 316 不鏽鋼">大容量隨行保溫杯|輕巧防漏 316 不鏽鋼</td><td class="nowrap-id">new_51513423876.jpg</td><td><span class="status-chip chip-info">待更新</span></td></tr>
<tr data-id="49913423712" data-stage="待更新"><td><input type="checkbox" data-row-check aria-label="选择商品 49913423712"></td><td>质感好物店</td><td class="nowrap-id">49913423712</td><td title="飯店級厚磅純棉浴巾|吸水快乾">飯店級厚磅純棉浴巾|吸水快乾</td><td class="nowrap-id">new_49913423712.jpg</td><td><span class="status-chip chip-info">待更新</span></td></tr>
<tr data-id="51813423856" data-stage="待更新"><td><input type="checkbox" data-row-check aria-label="选择商品 51813423856"></td><td>台北生活店</td><td class="nowrap-id">51813423856</td><td class="muted">缺新标题</td><td class="nowrap-id">new_51813423856.jpg</td><td><span class="status-chip chip-muted">内容不完整</span></td></tr>
<tr data-id="48663456321" data-stage="失败"><td><input type="checkbox" data-row-check aria-label="选择商品 48663456321"></td><td>质感好物店</td><td class="nowrap-id">48663456321</td><td title="暖光感應小夜燈|即插即用">暖光感應小夜燈|即插即用</td><td class="nowrap-id">new_48663456321.jpg</td><td><span class="status-chip chip-critical" title="上一轮提交失败:页面校验未通过">上一轮失败</span></td></tr>
<tr data-id="52313423890" data-stage="已更新"><td><input type="checkbox" data-row-check aria-label="选择商品 52313423890"></td><td>台北生活店</td><td class="nowrap-id">52313423890</td><td title="輕便折疊購物袋|大容量耐重">輕便折疊購物袋|大容量耐重</td><td class="nowrap-id">new_52313423890.jpg</td><td><span class="status-chip chip-success">已更新</span></td></tr>
</tbody>
</table>
<div class="table-empty">
<strong>当前筛选没有匹配的任务</strong>
<span>调整筛选条件,或清除筛选查看全部 5 条任务。</span>
</div>
</div>
<div class="zone-feedback">
<div class="selection-bar" data-selection-bar>
<span data-selection-text>已选 0 项</span>
<span class="push"></span>
<button class="btn-quiet" type="button" data-clear-selection>清除选择</button>
</div>
<!-- 预检结果:商品状态拦截与内容缺失合并展示(T-659 + T-662d)。
这是页面级持久信息,不使用 Toast。 -->
<div class="infobar info show" id="apply-precheck-bar">
<div class="body">
<strong>本轮可更新 3 条,已排除 2 条</strong>
<span class="detail">缺新标题 1 条;商品未上架 1 条。商品状态异常的记录不会提交,也不提供强制更新。</span>
</div>
<div class="actions">
<button type="button" data-open="apply-exclusion-dialog">查看排除明细</button>
</div>
</div>
</div>
<div class="zone-complete">
<span class="scope-note">本轮范围:当前筛选中通过预检的 3 条 · 提交后会修改线上商品,无法自动回滚</span>
<span class="push"></span>
<div class="actions">
<button type="button" data-stop="apply" disabled>停止</button>
<button type="button" data-open="apply-exclusion-dialog">检查本轮更新</button>
<button class="btn-caution" type="button" data-open="apply-confirm-dialog">开始更新</button>
</div>
</div>
</div>
</section>
<!-- ============================================================
商品套图
============================================================ -->
<section class="view" id="view-suite" role="tabpanel" aria-labelledby="tab-suite" tabindex="-1">
<header class="page-header">
<h1 class="page-title">商品套图</h1>
<span class="page-scope">为单个商品生成白底、场景、细节与卖点图</span>
<span class="push"></span>
<button type="button" data-toast="示意:已打开套图生成历史">生成历史</button>
</header>
<div class="split-layout">
<aside class="side-list" aria-label="套图项目列表">
<button class="list-item" type="button" aria-current="true">
<span class="list-main"><span class="list-title">51513423876</span><span class="list-meta">保温杯 · 6 张原图</span></span>
<span class="status-chip chip-success">已完成</span>
</button>
<button class="list-item" type="button" aria-current="false">
<span class="list-main"><span class="list-title">51813423856</span><span class="list-meta">收纳篮 · 4 张原图</span></span>
<span class="status-chip chip-info">生成中</span>
</button>
<button class="list-item" type="button" aria-current="false">
<span class="list-main"><span class="list-title">48663456321</span><span class="list-meta">小夜灯 · 5 张原图</span></span>
<span class="status-chip chip-muted">未开始</span>
</button>
</aside>
<section class="suite-main" aria-label="套图工作区">
<div class="gallery-scroll">
<div class="zone-prepare">
<div class="inline-field">
<label for="suite-shop">店铺</label>
<select id="suite-shop"><option>台北生活店</option></select>
</div>
<div class="inline-field">
<label for="suite-item">商品 ID</label>
<input type="text" id="suite-item" value="51513423876" style="width:170px">
</div>
<button type="button" data-toast="示意:正在后台拉取蝦皮主图">拉取蝦皮主图</button>
<span class="push"></span>
<button class="btn-quiet" type="button" data-toast="示意:已打开项目文件夹">打开文件夹</button>
<button class="btn-danger" type="button" data-open="delete-project-dialog">删除项目</button>
</div>
<div class="gallery-section">
<div class="gallery-head">
<strong>商品原图</strong>
<span class="muted" data-suite-count>已选 3 / 6 张</span>
<span class="push"></span>
<button class="btn-quiet" type="button" data-suite-all>全选</button>
<button class="btn-quiet" type="button" data-suite-none>取消全选</button>
</div>
<div class="photo-grid" data-suite-grid>
<button class="photo-thumb" type="button" aria-pressed="true" aria-label="原图 1,已选为参考图"><span class="tag">1</span></button>
<button class="photo-thumb" type="button" aria-pressed="true" aria-label="原图 2,已选为参考图"><span class="tag">2</span></button>
<button class="photo-thumb" type="button" aria-pressed="true" aria-label="原图 3,已选为参考图"><span class="tag">3</span></button>
<button class="photo-thumb" type="button" aria-pressed="false" aria-label="原图 4,未选中"><span class="tag"></span></button>
<button class="photo-thumb" type="button" aria-pressed="false" aria-label="原图 5,未选中"><span class="tag"></span></button>
<button class="photo-thumb" type="button" aria-pressed="false" aria-label="原图 6,未选中"><span class="tag"></span></button>
</div>
</div>
<div class="gallery-section">
<div class="gallery-head">
<strong>生成结果</strong>
<span class="muted">白底图 1 · 场景图 1 · 卖点图 1</span>
<span class="push"></span>
<button class="btn-quiet" type="button" data-toast="示意:已导出终选图片">导出终选</button>
</div>
<div class="photo-grid">
<button class="photo-thumb" type="button" aria-pressed="true" aria-label="白底图,已终选"><span class="tag">终选</span></button>
<button class="photo-thumb" type="button" aria-pressed="false" aria-label="场景图候选 1"><span class="tag">终选</span></button>
<button class="photo-thumb" type="button" aria-pressed="false" aria-label="卖点图候选 1"><span class="tag">终选</span></button>
</div>
</div>
</div>
<!-- 自定义网关下的入口拦截与已提交任务取回(T-677 第 4 节) -->
<div class="infobar caution" id="suite-gateway-bar">
<div class="body">
<strong>当前生成来源为自定义网关,商品套图已停用</strong>
<span class="detail">商品套图与「AI 帮写」仅支持默认网关。此前已提交到默认网关的任务仍可继续查询并取回,不会重复扣点。需要重新生成请到⑤把生成来源切回默认网关。</span>
</div>
<div class="actions">
<button type="button" data-toast="示意:正在查询已提交到默认网关的套图任务,不会新建任务,也不会再次扣点">继续查询已提交图片</button>
</div>
</div>
<div class="zone-complete">
<div class="inline-field">
<label for="suite-structure">套图结构</label>
<select id="suite-structure"><option>白底图 · 场景图 · 卖点图</option><option>白底图 · 场景图</option></select>
</div>
<div class="inline-field">
<label for="suite-count">每类数量</label>
<select id="suite-count"><option>1 张</option><option>2 张</option></select>
</div>
<span class="scope-note">3 类共 3 张 · 预计消耗 6 点</span>
<span class="push"></span>
<div class="actions">
<button type="button" data-toast="示意:AI 帮写将理解已选的 3 张原图" data-suite-gated>AI 帮写</button>
<button class="btn-primary" type="button" data-open="suite-confirm-dialog" data-suite-gated>生成套图</button>
</div>
</div>
</section>
</div>
</section>
<!-- ============================================================
账号管理
============================================================ -->
<section class="view" id="view-accounts" role="tabpanel" aria-labelledby="tab-accounts" tabindex="-1">
<header class="page-header">
<h1 class="page-title">账号管理</h1>
<span class="page-scope">管理店铺账号、Chrome 数据目录与登录状态</span>
<span class="push"></span>
<button type="button" data-toast="示意:已打开新增账号窗口">新增账号</button>
</header>
<div class="split-layout">
<aside class="side-list" aria-label="账号列表">
<button class="list-item" type="button" aria-current="true">
<span class="list-main"><span class="list-title">台北生活店</span><span class="list-meta">taipei_life · 端口 9222</span></span>
<span class="status-chip chip-success">已登录</span>
</button>
<button class="list-item" type="button" aria-current="false">
<span class="list-main"><span class="list-title">质感好物店</span><span class="list-meta">quality_goods · 端口 9223</span></span>
<span class="status-chip chip-muted">未检测</span>
</button>
<button class="list-item" type="button" aria-current="false">
<span class="list-main"><span class="list-title">日常选物店</span><span class="list-meta">daily_choice · 端口 9224</span></span>
<span class="status-chip chip-critical">未登录</span>
</button>
</aside>
<section class="detail-pane" aria-label="账号详情">
<div class="form-section">
<h2>账号详情</h2>
<div class="form-grid">
<div class="field"><label for="acc-name">账号名</label><input type="text" id="acc-name" value="台北生活店"></div>
<div class="field"><label for="acc-alias">别名</label><input type="text" id="acc-alias" value="taipei_life"></div>
<div class="field"><label for="acc-port">调试端口</label><input type="text" id="acc-port" value="9222"></div>
<div class="field span-2"><label for="acc-dir">Chrome 数据目录</label><input type="text" id="acc-dir" value="data/chrome/taipei_life"></div>
<div class="field"><label for="acc-login">登录状态</label><input type="text" id="acc-login" value="已登录(14:30 检测)" readonly></div>
</div>
</div>
<div class="form-section">
<h2>常用操作</h2>
<div class="button-row">
<button class="btn-primary" type="button" data-toast="示意:已复用台北生活店的 Chrome 数据目录启动">启动登录</button>
<button type="button" data-toast="台北生活店:已登录">检测登录</button>
<button type="button" data-toast="示意:已在桌面生成账号快捷方式">生成快捷方式</button>
<span class="separator" aria-hidden="true"></span>
<button class="btn-danger" type="button" data-open="delete-account-dialog">删除账号</button>
</div>
</div>
</section>
</div>
</section>
<!-- ============================================================
设置
============================================================ -->
<section class="view" id="view-settings" role="tabpanel" aria-labelledby="tab-settings" tabindex="-1">
<header class="page-header">
<h1 class="page-title">设置</h1>
<span class="page-scope">网关、模型、路径与更新安全参数</span>
<span class="push"></span>
<span class="status-chip chip-caution" id="settings-dirty" hidden>有未保存更改</span>
<button type="button" id="settings-discard" disabled>放弃更改</button>
<button class="btn-primary" type="button" id="settings-save" disabled title="Ctrl+S">保存设置</button>
</header>
<div class="detail-pane" style="margin: var(--space-3) var(--space-4) var(--space-4); border-radius: var(--radius-control);" data-settings-form>
<!-- 生成网关:来源二选一(T-677)。分区标题保留,承担长页面导航作用。 -->
<div class="form-section">
<div class="section-head">
<h2>生成网关</h2>
<div class="segmented" role="radiogroup" aria-label="生成网关来源" id="gateway-source">
<label data-source="cmhub"><input type="radio" name="gateway-source" value="cmhub" checked>默认网关</label>
<label data-source="direct"><input type="radio" name="gateway-source" value="direct">自定义网关</label>
</div>
<span class="push"></span>
<span class="field-hint">切换来源只标记未保存,点击「保存设置」后才生效。</span>
</div>
<!-- 默认网关面板 -->
<div class="gateway-panel" data-gateway-panel="cmhub">
<div class="form-grid">
<div class="field span-2"><label for="set-gateway">网关地址</label><input type="text" id="set-gateway" value="https://cm.833729.com"><span class="field-hint">只填写网关根地址,保存时自动规整路径。</span></div>
<div class="field"><label for="set-key">API Key</label><input type="password" id="set-key" value="default_gateway_key_placeholder"><span class="field-hint">以明文保存在本地配置文件中,请勿在共用电脑上使用。不会写入日志。</span></div>
<div class="field"><label for="set-text-alias">生文别名</label><select id="set-text-alias"><option>text-standard</option><option>text-pro</option></select></div>
<div class="field"><label for="set-image-alias">生图别名</label><select id="set-image-alias"><option>image-hd</option><option>image-fast</option></select></div>
<div class="field"><label for="set-vision-alias">图片理解别名</label><select id="set-vision-alias"><option>vision-standard</option></select></div>
<div class="field"><label for="set-connect">连接超时秒数</label><input type="text" id="set-connect" value="66"></div>
</div>
<div class="button-row" style="margin-top:var(--space-3)">
<button type="button" id="test-default-gateway">测试连接·查余额</button>
<span class="test-result" id="default-gateway-result" aria-live="polite">尚未测试</span>
</div>
<div class="button-row" style="margin-top:var(--space-3)">
<label style="display:flex;align-items:center;gap:var(--space-2);color:var(--text-primary);font-size:13px">
<input type="checkbox" id="set-check-balance" checked>批量生成前检查余额
</label>
</div>
</div>
<!-- 自定义网关面板 -->
<div class="gateway-panel" data-gateway-panel="direct" hidden>
<div class="infobar info show gateway-note">
<div class="body">
<strong>图片理解与商品套图仅默认网关支持</strong>
<span class="detail">切换到自定义网关后,②可以正常生成标题和封面;⑥商品套图的生成、重新生成和「AI 帮写」会被停用。此前已提交到默认网关的套图任务仍可在⑥继续查询并取回,不会重复计费。</span>
</div>
</div>
<div class="model-layout">
<div>
<label style="display:block;margin-bottom:var(--space-1)">自定义模型</label>
<div class="model-list" role="listbox" aria-label="自定义模型列表" id="model-list">
<button class="model-item" type="button" role="option" aria-current="true" aria-selected="true"><span><span class="name">本地 Qwen 文本</span><span class="meta">文本 · qwen2.5-72b-instruct</span></span><span class="status-chip chip-success">已启用</span></button>
<button class="model-item" type="button" role="option" aria-current="false" aria-selected="false"><span><span class="name">自建 SD 图像</span><span class="meta">图像 · sd3.5-large</span></span><span class="status-chip chip-success">已启用</span></button>
<button class="model-item" type="button" role="option" aria-current="false" aria-selected="false"><span><span class="name">备用文本网关</span><span class="meta">文本 · gpt-4o-mini</span></span><span class="status-chip chip-muted">已停用</span></button>
</div>
<div class="button-row" style="margin-top:var(--space-2)">
<button class="btn-quiet" type="button" data-model-crud="新增模型">新增</button>
<button class="btn-quiet" type="button" data-model-crud="保存模型">保存模型</button>
<button class="btn-quiet btn-danger" type="button" data-open="delete-model-dialog">删除</button>
</div>
<span class="field-hint">模型的新增、保存和删除会立即写入配置,不需要点「保存设置」。</span>
</div>
<div>
<div class="form-grid">
<div class="field"><label for="model-name">模型名称</label><input type="text" id="model-name" value="本地 Qwen 文本"></div>
<div class="field"><label for="model-category">类别</label><select id="model-category"><option>文本</option><option>图像</option></select></div>
<div class="field"><label for="model-enabled">启用状态</label><select id="model-enabled"><option>已启用</option><option>已停用</option></select></div>
<div class="field span-2"><label for="model-endpoint">接口地址</label><input type="text" id="model-endpoint" value="https://api.example-llm.local/v1"><span class="field-hint">填写 OpenAI 兼容服务的根地址,保存时自动补全路径。</span></div>
<div class="field"><label for="model-id">模型 ID</label><input type="text" id="model-id" value="qwen2.5-72b-instruct"></div>
<div class="field"><label for="model-key">API Key</label><input type="password" id="model-key" value="custom_gateway_key_placeholder"><span class="field-hint">以明文保存在本地配置文件中,不会写入日志。</span></div>
<div class="field"><label for="model-api-type">接口类型</label><select id="model-api-type"><option>对话接口(chat)</option><option>图片编辑接口(images_edits)</option><option>自动(auto)</option></select></div>
<div class="field"><label for="model-timeout">超时秒数</label><input type="text" id="model-timeout" value="120"></div>
<div class="field span-3"><span class="field-hint">接口类型说明:「对话接口」和「自动」都按对话请求发送,只有「图片编辑接口」按图片编辑请求发送。「自动」不会探测对方服务支持哪些接口,请按你的服务实际情况选择。</span></div>
</div>
<div class="button-row" style="margin-top:var(--space-3)">
<button type="button" id="test-custom-model">测试该模型</button>
<span class="test-result" id="custom-model-result" aria-live="polite">尚未测试</span>
</div>
</div>
</div>
<div class="form-grid" style="margin-top:var(--space-4)">
<div class="field"><label for="set-default-text">默认文本模型</label><select id="set-default-text"><option>本地 Qwen 文本</option><option>备用文本网关</option></select></div>
<div class="field"><label for="set-default-image">默认图像模型</label><select id="set-default-image"><option>自建 SD 图像</option></select></div>
<div class="field"><span class="field-hint">配置不完整也可以保存;②开始生成时会按本轮生成内容提示需要补齐哪一项。</span></div>
</div>
</div>
</div>
<div class="form-section">
<h2>角色与生成参数</h2>
<div class="form-grid">
<div class="field"><label for="set-title-conc">标题并发</label><input type="text" id="set-title-conc" value="4"></div>
<div class="field"><label for="set-image-conc">图片并发</label><input type="text" id="set-image-conc" value="5"></div>
<div class="field"><label for="set-retry">失败重试次数</label><input type="text" id="set-retry" value="2"></div>
<div class="field"><label for="set-image-wait">生图等待秒数</label><input type="text" id="set-image-wait" value="900"></div>
<div class="field"><label for="set-download">下载超时秒数</label><input type="text" id="set-download" value="650"></div>
</div>
</div>
<div class="form-section">
<h2>路径与更新安全</h2>
<div class="form-grid">
<div class="field span-2"><label for="set-chrome">Chrome 路径</label><input type="text" id="set-chrome" value="C:\Program Files\Google\Chrome\Application\chrome.exe"></div>
<div class="field"><label for="set-port">默认调试端口</label><input type="text" id="set-port" value="9222"></div>
<div class="field"><label for="set-parallel">多账号并行更新</label><select id="set-parallel"><option>已启用</option><option>已停用</option></select></div>
<div class="field"><label for="set-max-parallel">最大并行账号数</label><input type="text" id="set-max-parallel" value="2"></div>
<div class="field"><label for="set-max-batch">单次最大更新条数</label><input type="text" id="set-max-batch" value="100"></div>
</div>
</div>
</div>
</section>
</main>
<footer class="statusbar">
<span class="state" id="app-state">就绪</span>
<span id="status-message">② AI生成 · 当前筛选 5 条</span>
<span class="push"></span>
<button class="btn-quiet" type="button" data-open="prototype-notes-dialog">原型说明与验收边界</button>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<!-- ============================================================
对话框
============================================================ -->
<!-- ① 采集范围(T-664 定稿文案) -->
<dialog class="scope-dialog" id="collect-scope-dialog" aria-labelledby="collect-scope-title">
<div class="dialog-body">
<h2 class="dialog-title" id="collect-scope-title">选择本轮采集范围</h2>
<p class="dialog-copy">程序会逐个打开商品详情页检测并保存商品状态,再按所选范围读取旧标题与原封面。本步骤只读取页面,不修改线上商品。</p>
<div class="scope-options">
<button class="scope-option btn-primary" type="button" data-scope="normal_only" data-confirm="已按架上商品范围开始采集">
<strong>采集架上商品</strong>
<span>仅继续采集检测结果为正常的商品。</span>
</button>
<button class="scope-option btn-caution" type="button" data-scope="all" data-confirm="已按全部商品范围开始采集">
<strong>采集全部商品</strong>
<span>包括未上架、审核中和状态未知商品,可能增加采集时间,但本步骤不消耗 AI 点数。</span>
</button>
</div>
</div>
<div class="dialog-actions"><button type="button" data-close>取消</button></div>
</dialog>
<!-- ② 生成范围(T-665 定稿文案) -->
<dialog class="scope-dialog" id="generate-scope-dialog" aria-labelledby="generate-scope-title">
<div class="dialog-body">
<h2 class="dialog-title" id="generate-scope-title">选择本轮生成范围</h2>
<p class="dialog-copy">当前筛选包含 5 条商品,其中架上商品 4 条、状态异常 1 条。</p>
<div class="scope-options">
<button class="scope-option btn-primary" type="button" data-scope="normal_only" data-confirm="已按架上商品范围开始生成" data-run="generate">
<strong>生成架上商品</strong>
<span>仅把检测结果为正常的 4 条商品交给 AI。</span>
</button>
<button class="scope-option btn-caution" type="button" data-scope="all" data-confirm="已按全部商品范围开始生成" data-run="generate">
<strong>生成全部商品</strong>
<span>会把未上架、审核中和状态未知商品也交给 AI,可能额外消耗点数。</span>
</button>
</div>
</div>
<div class="dialog-actions"><button type="button" data-close>取消</button></div>
</dialog>
<!-- ③ 更新确认:高影响、不可逆,默认焦点在取消 -->
<dialog id="apply-confirm-dialog" aria-labelledby="apply-confirm-title">
<div class="dialog-body">
<h2 class="dialog-title" id="apply-confirm-title">更新 3 件蝦皮商品?</h2>
<p class="dialog-copy">将把新标题和新封面提交到蝦皮线上商品。提交后无法由本程序自动回滚,需要到蝦皮賣家中心手动改回。</p>
<div class="breakdown">
<div class="breakdown-row"><span>更新内容</span><span class="count">标题和封面</span></div>
<div class="breakdown-row"><span>涉及账号</span><span class="count">2 个</span></div>
<div class="breakdown-row"><span>通过预检的商品</span><span class="count">3 件</span></div>
<div class="breakdown-row"><span class="sample">51513423876、49913423712、52313423890</span></div>
</div>
</div>
<div class="dialog-actions">
<button type="button" data-close data-default-focus>取消</button>
<button class="btn-caution" type="button" data-confirm="已创建本轮更新计划,正在检测账号登录状态">更新 3 件商品</button>
</div>
</dialog>
<!-- ③ 排除明细 -->
<dialog class="wide" id="apply-exclusion-dialog" aria-labelledby="apply-exclusion-title">
<div class="dialog-body">
<h2 class="dialog-title" id="apply-exclusion-title">本轮更新预检结果</h2>
<p class="dialog-copy">共 5 条记录进入预检,可更新 3 条,排除 2 条。同一条记录只计一次排除,按原因优先级归类。</p>
<div class="breakdown">
<div class="breakdown-row"><span class="status-chip chip-success">可更新</span><span class="sample">51513423876、49913423712、52313423890</span><span class="count">3</span></div>
<div class="breakdown-row"><span class="status-chip chip-muted">缺新标题</span><span class="sample">51813423856</span><span class="count">1</span></div>
<div class="breakdown-row"><span class="status-chip chip-caution">商品未上架</span><span class="sample">48663456321</span><span class="count">1</span></div>
<div class="breakdown-row"><span class="status-chip chip-muted">审核中</span><span class="sample">—</span><span class="count">0</span></div>
<div class="breakdown-row"><span class="status-chip chip-muted">状态未知</span><span class="sample">—</span><span class="count">0</span></div>
</div>
<p class="dialog-copy" style="margin-top:var(--space-4)">商品状态异常的记录不会提交到蝦皮,也不提供强制更新入口。需要处理这些商品时,请先在蝦皮賣家中心恢复上架,再回到①重新采集刷新状态。</p>
</div>
<div class="dialog-actions"><button type="button" data-close data-default-focus>关闭</button></div>
</dialog>
<!-- 无效行明细 -->
<dialog class="wide" id="invalid-rows-dialog" aria-labelledby="invalid-rows-title">
<div class="dialog-body">
<h2 class="dialog-title" id="invalid-rows-title">3 行未能导入</h2>
<p class="dialog-copy">以下行已跳过,其余 125 行已正常导入。修正后可用“追加导入到当前批次”重新导入这些行。</p>
<div class="breakdown">
<div class="breakdown-row"><span class="sample">第 12 行</span><span>商品 ID 为空</span></div>
<div class="breakdown-row"><span class="sample">第 47 行</span><span>商品 ID 含非数字字符</span></div>
<div class="breakdown-row"><span class="sample">第 88 行</span><span>店铺别名 unknown_shop 未匹配到账号</span></div>
</div>
</div>
<div class="dialog-actions">
<button type="button" data-close data-default-focus>关闭</button>
<button type="button" data-confirm="示意:已导出无效行清单">导出清单</button>
</div>
</dialog>
<!-- 替换导入:会丢失不可恢复工作时才确认 -->
<dialog id="replace-import-dialog" aria-labelledby="replace-import-title">
<div class="dialog-body">
<h2 class="dialog-title" id="replace-import-title">替换当前批次数据?</h2>
<p class="dialog-copy">当前批次已有 3 条采集结果和 2 条生成结果。替换后这些结果会被移除,且无法恢复。选择“追加导入”可以保留现有结果。</p>
</div>
<div class="dialog-actions">
<button type="button" data-close data-default-focus>取消</button>
<button class="btn-danger" type="button" data-confirm="示意:已替换当前批次数据">替换并导入</button>
</div>
</dialog>
<!-- 重置生成结果:可逆性有限,需确认 -->
<dialog id="reset-results-dialog" aria-labelledby="reset-results-title">
<div class="dialog-body">
<h2 class="dialog-title" id="reset-results-title">重置所选商品的生成结果?</h2>
<p class="dialog-copy">已生成的新标题和新封面会被清空,需要重新消耗点数才能再次生成。原图与旧标题不受影响。</p>
</div>
<div class="dialog-actions">
<button type="button" data-close data-default-focus>取消</button>
<button class="btn-caution" type="button" data-confirm="已重置所选商品的生成结果">重置生成结果</button>
</div>
</dialog>
<!-- 破坏性:删除账号 -->
<dialog id="delete-account-dialog" aria-labelledby="delete-account-title">
<div class="dialog-body">
<h2 class="dialog-title" id="delete-account-title">删除账号“台北生活店”?</h2>
<p class="dialog-copy">将从本程序移除该账号配置。Chrome 数据目录 data/chrome/taipei_life 会保留在磁盘,需要手动清理。已导入且属于该账号的 62 条任务会失去账号匹配,无法继续采集或更新。</p>
</div>
<div class="dialog-actions">
<button type="button" data-close data-default-focus>取消</button>
<button class="btn-danger" type="button" data-confirm="示意:已删除账号台北生活店">删除账号</button>
</div>
</dialog>
<!-- 破坏性:删除自定义模型(立即写入,不等待「保存设置」) -->
<dialog id="delete-model-dialog" aria-labelledby="delete-model-title">
<div class="dialog-body">
<h2 class="dialog-title" id="delete-model-title">删除模型“本地 Qwen 文本”?</h2>
<p class="dialog-copy">将立即从自定义模型配置中删除,不需要点「保存设置」,也无法通过「放弃更改」恢复。如果它正被设为默认文本模型,②生成标题时会提示去⑤补齐。</p>
</div>
<div class="dialog-actions">
<button type="button" data-close data-default-focus>取消</button>
<button class="btn-danger" type="button" data-confirm="已删除模型「本地 Qwen 文本」(已立即写入配置)">删除模型</button>
</div>
</dialog>
<!-- 破坏性:删除套图项目 -->
<dialog id="delete-project-dialog" aria-labelledby="delete-project-title">
<div class="dialog-body">
<h2 class="dialog-title" id="delete-project-title">删除套图项目 51513423876?</h2>
<p class="dialog-copy">将永久删除该项目的 6 张原图和 3 张生成结果,无法恢复。已导出到其他位置的图片不受影响。</p>
</div>
<div class="dialog-actions">
<button type="button" data-close data-default-focus>取消</button>
<button class="btn-danger" type="button" data-confirm="示意:已删除套图项目">删除项目</button>
</div>
</dialog>
<!-- 套图生成确认:高成本 -->
<dialog id="suite-confirm-dialog" aria-labelledby="suite-confirm-title">
<div class="dialog-body">
<h2 class="dialog-title" id="suite-confirm-title">生成商品套图?</h2>
<p class="dialog-copy">将使用已选的 3 张原图,生成白底图、场景图和卖点图各 1 张,预计消耗 6 点。生成期间可以切换到其他标签页继续工作。</p>
</div>
<div class="dialog-actions">
<button type="button" data-close>取消</button>
<button class="btn-primary" type="button" data-confirm="商品套图已加入生成队列" data-default-focus>开始生成</button>
</div>
</dialog>
<!-- 停止确认:任务运行中切换/关闭时的协议演示 -->
<dialog id="notes-unsaved-dialog" aria-labelledby="notes-unsaved-title">
<div class="dialog-body">
<h2 class="dialog-title" id="notes-unsaved-title">保存设置更改?</h2>
<p class="dialog-copy">设置中有未保存的更改。离开前可以保存、放弃,或留在设置页继续编辑。</p>
</div>
<div class="dialog-actions">
<button class="push" type="button" data-close data-default-focus>取消</button>
<button type="button" data-unsaved="discard">不保存</button>
<button class="btn-primary" type="button" data-unsaved="save">保存</button>
</div>
</dialog>
<!-- 原型说明与验收边界 -->
<dialog class="wide" id="prototype-notes-dialog" aria-labelledby="prototype-notes-title">
<div class="dialog-body">
<h2 class="dialog-title" id="prototype-notes-title">原型说明与验收边界</h2>
<p class="dialog-copy">本页是 HTML 交互原型,用于确认信息架构、命令分区、状态反馈和视觉方向。数据为模拟数据,不连接 cmhub、蝦皮或本地数据库,任何按钮都不会产生真实副作用。</p>
<h3 style="margin:var(--space-4) 0 var(--space-1);font-size:13px">本原型已对齐的产品决策</h3>
<ul class="assumption-list">
<li><strong>①②③ 均不设商品状态列和商品状态下拉筛选</strong>(T-662b/c/d 非目标)。商品状态通过①采集前的范围确认、②生成前的范围确认,以及③的后台预检拦截来表达。</li>
<li><strong>①②范围确认使用纵向全宽按钮</strong>,最小内容宽度 520px,扩大范围选项为警示橙色描边而非红色,语义为非破坏性(T-663)。</li>
<li>按钮文案为<strong>「采集架上商品 / 采集全部商品」</strong>与<strong>「生成架上商品 / 生成全部商品」</strong>(T-664、T-665)。</li>
<li><strong>③不提供任何强制更新异常商品的入口</strong>,商品状态异常一律后台拦截(T-662d)。</li>
<li>历史批次已由数据库迁移默认为架上商品,<strong>不出现「待检测」状态和状态重检入口</strong>(T-667)。</li>
<li>⑤「生成网关」分区提供<strong>互斥选中态的「默认网关 / 自定义网关」</strong>来源选择器与双面板;切换只标记未保存,点「保存设置」才生效;模型的新增/保存/删除立即写入,「放弃更改」不回滚这些操作。⑤与②的可见文案不再出现 <span class="nowrap-id">cmhub</span> 字样(T-677)。</li>
<li>切到自定义网关后,②照常生成,⑥商品套图的生成与「AI 帮写」停用,但<strong>保留「继续查询已提交图片」入口</strong>取回已扣点任务(T-677 第 4 节)。</li>
</ul>
<h3 style="margin:var(--space-4) 0 var(--space-1);font-size:13px">仍需确认的假设</h3>
<ul class="assumption-list">
<li>表格新增<strong>选择复选框列</strong>与批量命令(仅重新采集所选 / 仅生成所选 / 重置所选生成结果)。当前实现以“当前筛选范围”为唯一作用域,此项为新增交互,需确认是否纳入范围。</li>
<li>①数据准备区的<strong>「追加导入 / 替换当前批次」</strong>区分,以及替换前的确认,需确认与现有导入行为是否一致。</li>
<li>③的<strong>预检结果信息条</strong>常驻在表格下方,替代原先只在点击后弹窗告知的方式。</li>
<li>部分成功后的<strong>「查看失败项 / 重试失败项」</strong>入口位置。</li>
</ul>
<h3 style="margin:var(--space-4) 0 var(--space-1);font-size:13px">不能仅靠本原型验收的内容</h3>
<ul class="assumption-list">
<li>原生标题栏、系统菜单、Snap Layouts、多窗口与任务栏行为;本页标题栏仅为示意。</li>
<li>Qt 原生控件的精确尺寸、字体栅格化、输入法与系统主题差异。</li>
<li>Mica/Acrylic 材质、真实 Windows 高对比度主题、多显示器 DPI 切换。</li>
<li>Narrator 与 UI Automation 树的最终语义。</li>
<li>大批量数据下的虚拟化、线程、内存与启动性能。</li>
</ul>
<p class="dialog-copy" style="margin-top:var(--space-4)">目标实现:PySide6 6.5.3(Qt Widgets)。颜色 Token 取自 <span class="nowrap-id">app/gui/widgets.py</span>,便于交接时直接映射到现有 QSS 常量。</p>
</div>
<div class="dialog-actions"><button type="button" data-close data-default-focus>关闭</button></div>
</dialog>
<script>
(function () {
'use strict';
var root = document.documentElement;
var toast = document.getElementById('toast');
var toastTimer = null;
var statusMessage = document.getElementById('status-message');
var appState = document.getElementById('app-state');
var lastFocused = null;
var VIEW_LABELS = {
collect: '① 导入采集',
generate: '② AI生成',
apply: '③ 更新蝦皮',
suite: '商品套图',
accounts: '账号管理',
settings: '设置'
};
/* ---------- Toast:仅用于无关键信息的短反馈 ---------- */
function showToast(message) {
toast.textContent = message;
toast.classList.add('show');
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () { toast.classList.remove('show'); }, 4000);
}
/* ---------- 标签栏:roving tabindex + 方向键 ---------- */
var tabs = Array.prototype.slice.call(document.querySelectorAll('.tab-button'));
var views = Array.prototype.slice.call(document.querySelectorAll('.view'));
var currentView = 'generate';
function activateView(name, moveFocus) {
currentView = name;
tabs.forEach(function (tab) {
var selected = tab.dataset.view === name;
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
tab.tabIndex = selected ? 0 : -1;
if (selected && moveFocus) { tab.focus(); }
});
views.forEach(function (view) { view.classList.toggle('active', view.id === 'view-' + name); });
updateStatus();
}
tabs.forEach(function (tab) {
tab.addEventListener('click', function () { activateView(tab.dataset.view, false); });
tab.addEventListener('keydown', function (event) {
var index = tabs.indexOf(tab);
var next = null;
if (event.key === 'ArrowRight') { next = tabs[(index + 1) % tabs.length]; }
else if (event.key === 'ArrowLeft') { next = tabs[(index - 1 + tabs.length) % tabs.length]; }
else if (event.key === 'Home') { next = tabs[0]; }
else if (event.key === 'End') { next = tabs[tabs.length - 1]; }
if (next) { event.preventDefault(); activateView(next.dataset.view, true); }
});
});
function updateStatus() {
var view = document.getElementById('view-' + currentView);
var count = view ? view.querySelector('[data-result-count]') : null;
statusMessage.textContent = VIEW_LABELS[currentView] + (count && count.textContent ? ' · ' + count.textContent : '');
}
/* ---------- 表格:当前行 / 选择集 / 勾选集分离 ---------- */
var tables = {};
Array.prototype.slice.call(document.querySelectorAll('[data-table]')).forEach(function (table) {
var key = table.dataset.table;
var tbody = table.querySelector('tbody');
var rows = Array.prototype.slice.call(tbody.rows);
var shell = table.closest('.table-shell');
var scope = document.querySelector('[data-table-scope="' + key + '"]');
var view = table.closest('.view');
var state = {
table: table, tbody: tbody, rows: rows, shell: shell, scope: scope, view: view,
anchor: null, sortKey: null, sortDir: 0
};
tables[key] = state;
rows.forEach(function (row) {
row.tabIndex = -1;
row.setAttribute('aria-selected', 'false');
/* 行单击只设置当前行与选择,不执行任何业务副作用。
这样双击产生的两次单击不会重复触发命令。 */
row.addEventListener('click', function (event) {
if (event.target.closest('input, button, a, select')) { return; }
setCurrent(state, row);
if (event.shiftKey && state.anchor) {
selectRange(state, state.anchor, row);
} else if (event.ctrlKey || event.metaKey) {
setSelected(state, row, row.getAttribute('aria-selected') !== 'true');
state.anchor = row;
} else {
state.rows.forEach(function (item) { setSelected(state, item, item === row); });
state.anchor = row;
}
syncSelection(state);
});
/* 双击 = 唯一的非破坏性默认命令(打开详情),同时提供 Enter 等价路径 */
row.addEventListener('dblclick', function (event) {
if (event.target.closest('input, button, a, select')) { return; }
openRowDefault(row);
});
row.addEventListener('keydown', function (event) {
if (event.key === 'Enter') { event.preventDefault(); openRowDefault(row); }
else if (event.key === ' ') {
event.preventDefault();
setSelected(state, row, row.getAttribute('aria-selected') !== 'true');
syncSelection(state);
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
var visible = visibleRows(state);
var pos = visible.indexOf(row);
var target = visible[event.key === 'ArrowDown' ? pos + 1 : pos - 1];
if (target) { setCurrent(state, target); target.focus(); }
}
});
var check = row.querySelector('[data-row-check]');
if (check) {
check.addEventListener('change', function () {
setSelected(state, row, check.checked);
state.anchor = row;
syncSelection(state);
});
}
});
/* 表头三态全选,范围 = 当前筛选结果 */
var selectAll = table.querySelector('[data-select-all]');
if (selectAll) {
selectAll.addEventListener('change', function () {
visibleRows(state).forEach(function (row) { setSelected(state, row, selectAll.checked); });
syncSelection(state);
});
}
/* 排序:改变视图顺序,不改变记录身份,选择集按 ID 保持 */
Array.prototype.slice.call(table.querySelectorAll('th.sortable')).forEach(function (th) {
th.addEventListener('click', function () { toggleSort(state, th); });
th.tabIndex = 0;
th.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleSort(state, th); }
});
});
if (scope) {
var search = scope.querySelector('[data-search]');
var stageFilter = scope.querySelector('[data-filter-stage]');
if (search) { search.addEventListener('input', function () { applyFilter(state); }); }
if (stageFilter) { stageFilter.addEventListener('change', function () { applyFilter(state); }); }
var clearBtn = scope.querySelector('[data-clear-filter]');
if (clearBtn) {
clearBtn.addEventListener('click', function () {
if (search) { search.value = ''; }
if (stageFilter) { stageFilter.value = ''; }
applyFilter(state);
showToast('已清除筛选条件');
});
}
var refreshBtn = scope.querySelector('[data-refresh]');
if (refreshBtn) {
refreshBtn.addEventListener('click', function () {
applyFilter(state);
showToast('列表已刷新(示意:不会重新打开商品页面)');
});
}
}
if (view) {
var clearSelection = view.querySelector('[data-clear-selection]');
if (clearSelection) {
clearSelection.addEventListener('click', function () {
state.rows.forEach(function (row) { setSelected(state, row, false); });
syncSelection(state);
});
}
}
applyFilter(state);
});
function openRowDefault(row) {
showToast('示意:打开商品 ' + row.dataset.id + ' 的详情(非破坏性默认命令)');
}
function visibleRows(state) {
return state.rows.filter(function (row) { return row.style.display !== 'none'; });
}
function setCurrent(state, row) {
state.rows.forEach(function (item) { item.classList.toggle('current', item === row); });
row.tabIndex = 0;
state.rows.forEach(function (item) { if (item !== row) { item.tabIndex = -1; } });
}
function setSelected(state, row, selected) {
row.setAttribute('aria-selected', selected ? 'true' : 'false');
var check = row.querySelector('[data-row-check]');
if (check) { check.checked = selected; }
}
function selectRange(state, from, to) {
var visible = visibleRows(state);
var a = visible.indexOf(from);
var b = visible.indexOf(to);
if (a < 0 || b < 0) { return; }
var start = Math.min(a, b);
var end = Math.max(a, b);
visible.forEach(function (row, index) { setSelected(state, row, index >= start && index <= end); });
}
/* 选择工具栏显示准确数量,并区分“当前筛选下不可见的已选项” */
function syncSelection(state) {
var selected = state.rows.filter(function (row) { return row.getAttribute('aria-selected') === 'true'; });
var hidden = selected.filter(function (row) { return row.style.display === 'none'; });
var bar = state.view.querySelector('[data-selection-bar]');
var text = state.view.querySelector('[data-selection-text]');
if (bar) { bar.classList.toggle('show', selected.length > 0); }
if (text) {
text.textContent = hidden.length
? '已选 ' + selected.length + ' 项,其中 ' + hidden.length + ' 项在当前筛选下不可见'
: '已选 ' + selected.length + ' 项';
}
var selectAll = state.table.querySelector('[data-select-all]');
if (selectAll) {
var visible = visibleRows(state);
var visibleSelected = visible.filter(function (row) { return row.getAttribute('aria-selected') === 'true'; });
selectAll.checked = visible.length > 0 && visibleSelected.length === visible.length;
selectAll.indeterminate = visibleSelected.length > 0 && visibleSelected.length < visible.length;
}
}
function applyFilter(state) {
var search = state.scope ? state.scope.querySelector('[data-search]') : null;
var stageFilter = state.scope ? state.scope.querySelector('[data-filter-stage]') : null;
var term = search ? search.value.trim() : '';
var stage = stageFilter ? stageFilter.value : '';
var matched = 0;
state.rows.forEach(function (row) {
var okTerm = !term || row.dataset.id.indexOf(term) !== -1;
var okStage = !stage || row.dataset.stage === stage;
var show = okTerm && okStage;
row.style.display = show ? '' : 'none';
if (show) { matched += 1; }
});
var hasFilter = Boolean(term || stage);
state.shell.classList.toggle('is-empty', matched === 0);
var empty = state.shell.querySelector('.table-empty');
if (empty && matched === 0) {
empty.querySelector('strong').textContent = hasFilter ? '当前筛选没有匹配的任务' : '还没有导入任何任务';
empty.querySelector('span').textContent = hasFilter
? '调整筛选条件,或清除筛选查看全部 ' + state.rows.length + ' 条任务。'
: '先在上方选择 Excel 文件并导入,任务会显示在这里。';
}
var counter = state.scope ? state.scope.querySelector('[data-result-count]') : null;
if (counter) {
counter.textContent = hasFilter
? '筛选结果 ' + matched + ' / ' + state.rows.length + ' 条'
: '共 ' + state.rows.length + ' 条';
}
var note = state.view.querySelector('[data-scope-note]');
if (note && note.dataset.template !== 'off') {
note.textContent = note.textContent.replace(/当前筛选 \d+ 条/, '当前筛选 ' + matched + ' 条');
}
syncSelection(state);
updateStatus();
}
function toggleSort(state, th) {
var key = th.dataset.sortKey;
var dir = (state.sortKey === key && state.sortDir === 1) ? -1 : 1;
state.sortKey = key;
state.sortDir = dir;
Array.prototype.slice.call(state.table.querySelectorAll('th')).forEach(function (item) {
if (item.hasAttribute('aria-sort')) { item.setAttribute('aria-sort', 'none'); }
});
th.setAttribute('aria-sort', dir === 1 ? 'ascending' : 'descending');
var index = Array.prototype.indexOf.call(th.parentElement.children, th);
var sorted = state.rows.slice().sort(function (a, b) {
var av = key === 'id' ? a.dataset.id : a.cells[index].textContent.trim();
var bv = key === 'id' ? b.dataset.id : b.cells[index].textContent.trim();
return av.localeCompare(bv, 'zh-Hans-CN') * dir;
});
sorted.forEach(function (row) { state.tbody.appendChild(row); });
state.rows = sorted;
state.anchor = null;
}
/* ---------- 对话框:焦点管理与返回调用者 ---------- */
document.querySelectorAll('[data-open]').forEach(function (button) {
button.addEventListener('click', function () {
var dialog = document.getElementById(button.dataset.open);
if (!dialog || dialog.open) { return; }
lastFocused = button;
dialog.showModal();
var target = dialog.querySelector('[data-default-focus]') || dialog.querySelector('button:not([data-close])') || dialog.querySelector('button');
if (target) { target.focus(); }
});
});
document.querySelectorAll('dialog').forEach(function (dialog) {
dialog.addEventListener('close', function () {
if (lastFocused && document.contains(lastFocused)) { lastFocused.focus(); }
lastFocused = null;
});
});
document.querySelectorAll('[data-close]').forEach(function (button) {
button.addEventListener('click', function () { button.closest('dialog').close('cancel'); });
});
document.querySelectorAll('[data-confirm]').forEach(function (button) {
button.addEventListener('click', function () {
var dialog = button.closest('dialog');
if (dialog) { dialog.close('confirm'); }
showToast(button.dataset.confirm);
if (button.dataset.run === 'generate') { startGenerateRun(); }
});
});
/* 替换导入前才确认,追加导入直接执行 */
var importButton = document.querySelector('[data-confirm-import]');
if (importButton) {
importButton.addEventListener('click', function () {
var mode = document.getElementById('collect-import-mode').value;
if (mode.indexOf('替换') !== -1) {
lastFocused = importButton;
var dialog = document.getElementById('replace-import-dialog');
dialog.showModal();
dialog.querySelector('[data-default-focus]').focus();
} else {
showToast('示意:已追加导入到当前批次');
}
});
}
document.querySelectorAll('[data-dismiss]').forEach(function (button) {
button.addEventListener('click', function () {
var bar = document.getElementById(button.dataset.dismiss);
if (bar) { bar.classList.remove('show'); }
});
});
document.querySelectorAll('[data-toast]').forEach(function (button) {
button.addEventListener('click', function () { showToast(button.dataset.toast); });
});
/* ---------- ② 生成任务:完整任务状态机 ---------- */
var run = { state: 'idle', timer: null, title: 0, image: 0, total: 5 };
var progressDeck = document.getElementById('generate-progress');
var generateLog = document.getElementById('generate-log');
var startButton = document.getElementById('start-generate');
var stopButton = document.querySelector('[data-stop="generate"]');
var resultBar = document.getElementById('generate-result-bar');
var generateTab = document.getElementById('tab-generate');
function logLine(text) {
var stamp = new Date().toTimeString().slice(0, 8);
generateLog.textContent = stamp + ' ' + text;
}
function setRunState(next) {
run.state = next;
var busy = next === 'running' || next === 'cancelling';
startButton.disabled = busy;
stopButton.disabled = !busy || next === 'cancelling';
generateTab.classList.toggle('has-task', busy);
appState.classList.toggle('busy', busy);
appState.textContent = busy ? (next === 'cancelling' ? '正在停止' : '生成中') : '就绪';
if (next === 'cancelling') { stopButton.textContent = '正在停止…'; }
else { stopButton.textContent = '停止'; }
}
function updateBar(kind, done) {
var label = document.querySelector('[data-progress-label="' + kind + '"]');
var bar = document.querySelector('[data-progress-bar="' + kind + '"]');
var meta = document.querySelector('[data-progress-meta="' + kind + '"]');
var name = kind === 'title' ? '标题' : '图片';
label.textContent = name + ' ' + done + '/' + run.total;
bar.style.width = (done / run.total * 100) + '%';
bar.classList.toggle('done', done === run.total);
meta.textContent = done === run.total ? '已完成' : '进行中 · 约 ' + ((run.total - done) * 12) + ' 秒';
}
function startGenerateRun() {
if (run.state === 'running') { return; }
run.title = 0;
run.image = 0;
resultBar.classList.remove('show');
progressDeck.hidden = false;
setRunState('running');
updateBar('title', 0);
updateBar('image', 0);
logLine('[开始] 本轮 ' + run.total + ' 条:标题 ' + run.total + ',图片 ' + run.total + '。');
window.clearInterval(run.timer);
run.timer = window.setInterval(function () {
if (run.state === 'cancelling') {
window.clearInterval(run.timer);
finishRun('cancelled');
return;
}
if (run.title < run.total) { run.title += 1; updateBar('title', run.title); }
else if (run.image < run.total - 1) { run.image += 1; updateBar('image', run.image); }
else {
/* 最后一条图片任务失败:进度停在 4/5,由部分成功信息条说明 */
window.clearInterval(run.timer);
finishRun('partial');
return;
}
logLine('[进行] 标题 ' + run.title + '/' + run.total + ',图片 ' + run.image + '/' + run.total + '。');
}, 700);
}
/* 完成、失败、取消是终态,不靠进度条消失判断 */
function finishRun(outcome) {
setRunState('idle');
var title = resultBar.querySelector('[data-result-title]');
var detail = resultBar.querySelector('[data-result-detail]');
resultBar.classList.remove('caution', 'critical', 'info');
if (outcome === 'cancelled') {
resultBar.classList.add('info', 'show');
title.textContent = '本轮生成已停止:已完成 ' + (run.title + run.image) + ' 项';
detail.textContent = '已生成的标题和图片已保留,未开始的任务保持“待生成”。再次开始只会处理未完成的任务。';
logLine('[已停止] 用户请求停止,已完成结果保留。');
} else {
resultBar.classList.add('caution', 'show');
title.textContent = '本轮生成部分完成:成功 4,失败 1';
detail.textContent = '商品 48663456321 的图片下载超时,标题已保存。失败项的点数已消耗,重试会重新计费。';
document.querySelector('[data-progress-meta="image"]').textContent = '1 项失败';
logLine('[完成] 成功 4,失败 1。');
}
updateStatus();
}
if (stopButton) {
stopButton.addEventListener('click', function () {
if (run.state !== 'running') { return; }
setRunState('cancelling');
logLine('[停止中] 已请求停止,正在等待进行中的任务返回。');
});
}
/* ① 与 ③ 的停止按钮共享同一取消语义(原型内为占位) */
document.querySelectorAll('[data-stop]:not([data-stop="generate"])').forEach(function (button) {
button.addEventListener('click', function () { showToast('已请求停止,正在等待进行中的任务返回'); });
});
/* ---------- 商品套图:勾选与计数 ---------- */
var suiteGrid = document.querySelector('[data-suite-grid]');
if (suiteGrid) {
var thumbs = Array.prototype.slice.call(suiteGrid.querySelectorAll('.photo-thumb'));
var suiteCount = document.querySelector('[data-suite-count]');
function syncSuite() {
var picked = thumbs.filter(function (t) { return t.getAttribute('aria-pressed') === 'true'; });
picked.forEach(function (t, i) { t.querySelector('.tag').textContent = String(i + 1); });
suiteCount.textContent = '已选 ' + picked.length + ' / ' + thumbs.length + ' 张';
}
thumbs.forEach(function (thumb) {
thumb.addEventListener('click', function () {
thumb.setAttribute('aria-pressed', thumb.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
syncSuite();
});
});
document.querySelector('[data-suite-all]').addEventListener('click', function () {
thumbs.forEach(function (t) { t.setAttribute('aria-pressed', 'true'); });
syncSuite();
});
document.querySelector('[data-suite-none]').addEventListener('click', function () {
thumbs.forEach(function (t) { t.setAttribute('aria-pressed', 'false'); });
syncSuite();
});
syncSuite();
}
/* ---------- 列表/详情:单选当前项 ---------- */
document.querySelectorAll('.side-list').forEach(function (list) {
var items = Array.prototype.slice.call(list.querySelectorAll('.list-item'));
items.forEach(function (item) {
item.addEventListener('click', function () {
items.forEach(function (other) { other.setAttribute('aria-current', other === item ? 'true' : 'false'); });
});
});
});
/* ---------- 设置:脏状态与未保存协议 ---------- */
var settingsForm = document.querySelector('[data-settings-form]');
var settingsDirty = document.getElementById('settings-dirty');
var settingsSave = document.getElementById('settings-save');
var settingsDiscard = document.getElementById('settings-discard');
var isDirty = false;
function setDirty(value) {
isDirty = value;
settingsDirty.hidden = !value;
settingsSave.disabled = !value;
settingsDiscard.disabled = !value;
}
settingsForm.addEventListener('input', function () { setDirty(true); });
settingsForm.addEventListener('change', function () { setDirty(true); });
/* ---------- ⑤ 生成网关来源(T-677) ----------
savedSource 是已落盘的来源;pendingSource 只在点「保存设置」后才生效。
②的余额文案跟随 savedSource,不跟随未保存的选择。 */
var savedSource = 'cmhub';
var pendingSource = 'cmhub';
var sourceRadios = Array.prototype.slice.call(document.querySelectorAll('input[name="gateway-source"]'));
var generateBalance = document.getElementById('generate-balance');
var generateScopeText = document.getElementById('generate-scope-text');
function renderSourcePanels(source) {
document.querySelectorAll('[data-gateway-panel]').forEach(function (panel) {
panel.hidden = panel.dataset.gatewayPanel !== source;
});
document.querySelectorAll('#gateway-source label').forEach(function (label) {
label.classList.toggle('is-checked', label.dataset.source === source);
});
}
/* 已保存来源决定②的点数口径与⑥的可用性。
保存后立即刷新,不需要重启程序,也不等待用户重新进入⑥。 */
function renderSavedSource() {
var custom = savedSource === 'direct';
if (custom) {
generateBalance.className = 'status-chip chip-caution';
generateBalance.textContent = '自定义网关(不计点数,费用由服务商收取)';
generateScopeText.textContent = '通过自定义网关生成新标题与新封面,费用由服务商收取';
} else {
generateBalance.className = 'status-chip chip-success';
generateBalance.textContent = '默认网关余额:1,280 点';
generateScopeText.textContent = '通过默认网关生成新标题与新封面,会消耗点数';
}
document.getElementById('titlebar-gateway').textContent = custom ? '自定义网关 已配置' : '默认网关 已连接';
/* ⑥:拦截新建与新提交,但保留已扣点任务的取回入口 */
document.querySelectorAll('[data-suite-gated]').forEach(function (button) {
button.disabled = custom;
button.title = custom ? '商品套图仅支持默认网关,请到⑤把生成来源切回默认网关' : '';
});
document.getElementById('suite-gateway-bar').classList.toggle('show', custom);
}
sourceRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
pendingSource = radio.value;
renderSourcePanels(pendingSource);
/* dirty 由 settingsForm 的 change 监听统一标记,这里不单独清除,
避免把同页其他字段的未保存更改误判为已恢复 */
});
radio.addEventListener('focus', function () {
radio.closest('label').classList.add('is-focused');
});
radio.addEventListener('blur', function () {
radio.closest('label').classList.remove('is-focused');
});
});
/* 模型 CRUD 立即写入,与来源的待保存语义区分开 */
document.querySelectorAll('[data-model-crud]').forEach(function (button) {
button.addEventListener('click', function () {
showToast(button.dataset.modelCrud + '已立即写入配置,不需要点「保存设置」');
});
});
document.querySelectorAll('#model-list .model-item').forEach(function (item, index, items) {
item.addEventListener('click', function () {
Array.prototype.slice.call(items).forEach(function (other) {
other.setAttribute('aria-current', other === item ? 'true' : 'false');
other.setAttribute('aria-selected', other === item ? 'true' : 'false');
});
});
});
var testDefault = document.getElementById('test-default-gateway');
if (testDefault) {
testDefault.addEventListener('click', function () {
var result = document.getElementById('default-gateway-result');
result.textContent = '连接中…';
testDefault.disabled = true;
window.setTimeout(function () {
result.textContent = '连接成功 · 余额 1,280 点';
testDefault.disabled = false;
}, 900);
});
}
var testCustom = document.getElementById('test-custom-model');
if (testCustom) {
testCustom.addEventListener('click', function () {
var result = document.getElementById('custom-model-result');
result.textContent = '连接中…';
testCustom.disabled = true;
window.setTimeout(function () {
result.textContent = '连接成功 · 已返回结果';
testCustom.disabled = false;
}, 900);
});
}
function saveSettings() {
var switched = pendingSource !== savedSource;
savedSource = pendingSource;
setDirty(false);
renderSavedSource();
showToast(switched
? (savedSource === 'direct'
? '设置已保存:生成来源已切换为自定义网关,商品套图已停用'
: '设置已保存:生成来源已切换为默认网关')
: '设置已保存');
}
/* 放弃更改只恢复来源和普通字段,不回滚已经立即保存的模型 CRUD */
function discardSettings() {
pendingSource = savedSource;
sourceRadios.forEach(function (radio) { radio.checked = radio.value === savedSource; });
renderSourcePanels(savedSource);
setDirty(false);
showToast('已恢复未保存的更改;已单独保存的模型不受影响');
}
settingsSave.addEventListener('click', saveSettings);
settingsDiscard.addEventListener('click', discardSettings);
renderSourcePanels(savedSource);
renderSavedSource();
document.querySelectorAll('[data-unsaved]').forEach(function (button) {
button.addEventListener('click', function () {
button.closest('dialog').close();
if (button.dataset.unsaved === 'save') { saveSettings(); } else { discardSettings(); }
});
});
/* ---------- 主题切换(用于评审浅色/深色,非产品功能) ---------- */
var themeToggle = document.getElementById('theme-toggle');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
function setTheme(dark) {
root.setAttribute('data-theme', dark ? 'dark' : 'light');
themeToggle.setAttribute('aria-pressed', dark ? 'true' : 'false');
themeToggle.textContent = dark ? '浅色主题' : '深色主题';
}
setTheme(prefersDark.matches);
themeToggle.addEventListener('click', function () {
setTheme(root.getAttribute('data-theme') !== 'dark');
});
/* ---------- 全局键盘 ---------- */
document.addEventListener('keydown', function (event) {
var ctrl = event.ctrlKey || event.metaKey;
var key = event.key.toLowerCase();
/* F6:在主要窗格间移动 */
if (event.key === 'F6') {
event.preventDefault();
var view = document.getElementById('view-' + currentView);
var panes = Array.prototype.slice.call(view.querySelectorAll('.zone-prepare, .zone-tablecmd, .table-shell, .zone-complete, .prompt-pane, .side-list, .detail-pane'));
var active = panes.findIndex(function (pane) { return pane.contains(document.activeElement); });
var next = panes[(active + 1) % panes.length];
if (next) {
var focusable = next.querySelector('button:not(:disabled), input, select, textarea, tr[tabindex="0"]');
if (focusable) { focusable.focus(); } else { next.focus(); }
}
return;
}
/* Ctrl+F:聚焦当前页搜索框 */
if (ctrl && key === 'f') {
var search = document.querySelector('#view-' + currentView + ' [data-search]');
if (search) { event.preventDefault(); search.focus(); search.select(); }
return;
}
/* F5:刷新当前页列表 */
if (event.key === 'F5') {
var refresh = document.querySelector('#view-' + currentView + ' [data-refresh]');
if (refresh) { event.preventDefault(); refresh.click(); }
return;
}
/* Ctrl+S:仅在设置页有保存语义 */
if (ctrl && key === 's') {
event.preventDefault();
if (currentView === 'settings') {
if (isDirty) { setDirty(false); showToast('设置已保存'); }
else { showToast('没有需要保存的更改'); }
} else {
showToast('当前页面没有可保存的内容,保存设置请切换到设置页');
}
}
});
/* 有未保存更改时离开设置页先询问 */
tabs.forEach(function (tab) {
tab.addEventListener('click', function (event) {
if (currentView === 'settings' && isDirty && tab.dataset.view !== 'settings') {
event.preventDefault();
event.stopPropagation();
lastFocused = tab;
var dialog = document.getElementById('notes-unsaved-dialog');
dialog.showModal();
dialog.querySelector('[data-default-focus]').focus();
}
}, true);
});
activateView('generate', false);
}());
</script>
</body>
</html>