Files
cmshoppe/docs/ui/workbench-redesign-v1.html
T

2295 lines
128 KiB
HTML
Raw Normal View History

<!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>