Files
cmshoppe/docs/ui/workbench-redesign-v1.html
T
chengmaandClaude Fable 5 e65a1595fe docs(ui): rebuild workbench prototype per Windows UI/UX spec
按 windows-ui-ux skill 的核心体验层、Windows 平台层和 Qt 适配方向重写
docs/ui/workbench-redesign-v1.html:

- 对齐已定稿产品决策:移除 ①②③ 的商品状态列与商品状态下拉筛选
  (T-662b/c/d 非目标),范围确认改为纵向全宽按钮 + 警示橙色描边
  (T-663),文案改为「采集/生成架上商品|全部商品」(T-664、T-665),
  ③ 改为后台预检拦截且无强制更新入口(T-662d),移除「待检测」状态
  与状态重检入口(T-667)。
- 统一页面骨架与命令分区:数据准备区、表格命令区、表格、状态反馈区、
  完成任务区;导入/刷新/选择集/工作流完成命令不再混区,每区一个主按钮。
- 补齐表格交互契约:选择复选框列、三态全选、当前行/焦点/选择集分离、
  Ctrl/Shift 多选、排序 aria-sort、隐藏选择计数、空与筛选无结果状态。
- 按干扰层级重排反馈:破坏性操作改真实确认框且默认聚焦取消,部分成功
  与预检结果改页面级信息条,停止增加 cancelling 中间态,Toast 仅承载
  非关键短反馈。
- 平台基线:语义 Token 对齐 app/gui/widgets.py 颜色常量与 4epx 间距,
  新增深色主题与高对比度近似,触控目标 ≥32epx,键盘补 tablist 方向键、
  F6 窗格切换、Ctrl+F、F5,Ctrl+S 仅在设置页生效并接未保存协议。
- 新增「原型说明与验收边界」对话框,记录已对齐决策、待确认假设,以及
  不能仅靠 HTML 验收的原生标题栏、材质、DPI、无障碍与性能项。

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

2034 lines
111 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>蝦皮圈優化助手 - 工作台视觉提案 v1</title>
<style>
/* ============================================================
语义 Token 层
颜色基线取自 app/gui/widgets.py 的实际常量,便于原型与 Qt 实现对齐:
COLOR_SUCCESS #1a7f37 / COLOR_DANGER #cf222e / COLOR_INFO #0969da
COLOR_PENDING #9a6700 / COLOR_MUTED #6e7781 / COLOR_WARNING #bc4c00
间距遵循 4 epx 节奏;圆角对齐 BUTTON_RADIUS_PX = 4。
============================================================ */
:root {
color-scheme: light dark;
--font-ui: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
--font-mono: "Cascadia Mono", Consolas, "Microsoft YaHei UI", monospace;
--surface-base: #f6f8fa;
--surface-layer: #ffffff;
--surface-subtle: #f6f8fa;
--surface-overlay: #ffffff;
--surface-hover: #eef4ff;
--surface-pressed: #d8dee4;
--surface-window: #e6eaef;
--text-primary: #24292f;
--text-secondary: #57606a;
--text-disabled: #8c959f;
--accent-default: #0969da;
--accent-hover: #0860c4;
--accent-pressed: #0757ad;
--accent-subtle: #ddf0ff;
--status-success: #1a7f37;
--status-success-bg: #e6f6ea;
--status-caution: #bc4c00;
--status-caution-bg: #fff3e8;
--status-critical: #cf222e;
--status-critical-bg: #ffebe9;
--status-info: #0969da;
--status-info-bg: #ddf0ff;
--status-pending: #9a6700;
--status-pending-bg: #fff8dc;
--status-muted: #6e7781;
--status-muted-bg: #eef1f4;
--stroke-control: #d0d7de;
--stroke-strong: #8c959f;
--stroke-divider: #e4e8ec;
--focus-stroke: #0969da;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--radius-control: 4px;
--radius-overlay: 8px;
--motion-fast: 120ms;
--motion-normal: 200ms;
--ease: cubic-bezier(.2, 0, 0, 1);
--shadow-overlay: 0 12px 32px rgba(20, 30, 40, .18);
--shadow-flyout: 0 4px 14px rgba(20, 30, 40, .12);
}
:root[data-theme="dark"] {
--surface-base: #22272e;
--surface-layer: #2d333b;
--surface-subtle: #262c34;
--surface-overlay: #2d333b;
--surface-hover: #363d47;
--surface-pressed: #444c56;
--surface-window: #1c2128;
--text-primary: #e6edf3;
--text-secondary: #adbac7;
--text-disabled: #768390;
--accent-default: #539bf5;
--accent-hover: #6cb0ff;
--accent-pressed: #4285d6;
--accent-subtle: #1c3d5c;
--status-success: #57ab5a;
--status-success-bg: #1b3428;
--status-caution: #e0823d;
--status-caution-bg: #3a2515;
--status-critical: #e5534b;
--status-critical-bg: #3c1c1a;
--status-info: #539bf5;
--status-info-bg: #1c3d5c;
--status-pending: #c69026;
--status-pending-bg: #3a2f14;
--status-muted: #909dab;
--status-muted-bg: #343a42;
--stroke-control: #444c56;
--stroke-strong: #6b7681;
--stroke-divider: #373e47;
--focus-stroke: #539bf5;
--shadow-overlay: 0 12px 32px rgba(0, 0, 0, .5);
--shadow-flyout: 0 4px 14px rgba(0, 0, 0, .4);
}
/* 高对比度近似:加粗描边、去除弱背景、强制系统色可覆盖。
真实 Windows 高对比度必须在 Qt 端用系统调色板复验。 */
@media (prefers-contrast: more) {
:root {
--stroke-control: #24292f;
--stroke-strong: #24292f;
--stroke-divider: #24292f;
--text-secondary: #24292f;
--surface-hover: #ffffff;
--status-muted-bg: transparent;
--status-info-bg: transparent;
--status-success-bg: transparent;
--status-caution-bg: transparent;
--status-critical-bg: transparent;
--status-pending-bg: transparent;
}
:root[data-theme="dark"] {
--stroke-control: #ffffff;
--stroke-strong: #ffffff;
--stroke-divider: #ffffff;
--text-secondary: #ffffff;
}
.status-chip, button, .zone-prepare, .zone-feedback, .table-shell, .infobar { border-width: 2px !important; }
}
* { box-sizing: border-box; }
body {
min-width: 900px;
min-height: 100vh;
margin: 0;
background: var(--surface-window);
color: var(--text-primary);
font-family: var(--font-ui);
font-size: 14px;
line-height: 1.45;
}
button, input, select, textarea { font: inherit; color: inherit; }
button, select, input, textarea { outline: none; }
:is(button, select, input, textarea, [tabindex], a):focus-visible {
outline: 2px solid var(--focus-stroke);
outline-offset: 1px;
}
/* ---- 按钮:最小 32 epx 高,主要目标 ≥ 40 epx 宽 ---- */
button {
min-height: 32px;
min-width: 72px;
padding: var(--space-1) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
cursor: pointer;
white-space: nowrap;
transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
button:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--stroke-strong); }
button:active:not(:disabled) { background: var(--surface-pressed); }
button:disabled { color: var(--text-disabled); border-color: var(--stroke-divider); cursor: not-allowed; }
.btn-primary { background: var(--accent-default); border-color: var(--accent-default); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-primary:active:not(:disabled) { background: var(--accent-pressed); }
.btn-primary:disabled { background: var(--surface-subtle); border-color: var(--stroke-divider); color: var(--text-disabled); }
/* 警示样式:范围扩大 / 线上写入。非破坏性,不使用 danger 红。
与 T-663/664/665 定稿一致(COLOR_WARNING #bc4c00 描边)。 */
.btn-caution { border-color: var(--status-caution); color: var(--status-caution); background: var(--surface-layer); }
.btn-caution:hover:not(:disabled) { background: var(--status-caution-bg); border-color: var(--status-caution); }
/* 破坏性样式:仅用于不可恢复删除 */
.btn-danger { border-color: var(--status-critical); color: var(--status-critical); background: var(--surface-layer); }
.btn-danger:hover:not(:disabled) { background: var(--status-critical-bg); }
.btn-quiet { border-color: transparent; background: transparent; color: var(--text-secondary); min-width: 0; }
.btn-quiet:hover:not(:disabled) { background: var(--surface-hover); border-color: transparent; color: var(--text-primary); }
.btn-icon { min-width: 32px; width: 32px; padding: 0; }
label { color: var(--text-secondary); font-size: 12px; }
select, input[type="text"], input[type="password"], textarea {
width: 100%;
min-height: 32px;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
}
select:hover, input:hover, textarea:hover { border-color: var(--stroke-strong); }
textarea { min-height: 120px; resize: vertical; line-height: 1.55; font-family: var(--font-ui); }
input[type="checkbox"] { width: 16px; height: 16px; min-height: 0; margin: 0; accent-color: var(--accent-default); cursor: pointer; }
/* ============================================================
应用外壳
============================================================ */
.app-frame {
width: min(1560px, calc(100vw - 32px));
height: min(960px, calc(100vh - 32px));
min-height: 640px;
margin: 16px auto;
display: grid;
grid-template-rows: 44px 42px minmax(0, 1fr) 30px;
overflow: hidden;
border: 1px solid var(--stroke-control);
border-radius: var(--radius-overlay);
background: var(--surface-base);
box-shadow: var(--shadow-overlay);
}
.titlebar {
display: flex;
align-items: center;
gap: var(--space-3);
padding: 0 var(--space-2) 0 var(--space-4);
background: var(--surface-layer);
border-bottom: 1px solid var(--stroke-divider);
user-select: none;
}
.brand-mark {
width: 22px; height: 22px;
display: grid; place-items: center;
border-radius: var(--radius-control);
background: #e8543f; color: #fff;
font-weight: 700; font-size: 12px;
}
.app-name { font-weight: 600; }
.titlebar .push { margin-left: auto; }
.gateway-state { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-secondary); }
.gateway-state::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--status-success); }
.window-actions { display: flex; gap: 2px; margin-left: var(--space-2); }
.window-actions button { min-width: 40px; width: 40px; min-height: 32px; border: 0; background: transparent; color: var(--text-secondary); }
.window-actions button:hover { background: var(--surface-hover); }
.window-actions button.close:hover { background: #c42b1c; color: #fff; }
/* ---- 标签栏:静态任务标签 ---- */
.tabbar {
display: flex;
align-items: stretch;
gap: 2px;
padding: 0 var(--space-3);
background: var(--surface-subtle);
border-bottom: 1px solid var(--stroke-divider);
}
.tab-button {
position: relative;
min-height: 40px;
min-width: 0;
padding: 0 var(--space-3);
border: 0;
border-radius: 0;
background: transparent;
color: var(--text-secondary);
font-weight: 500;
}
.tab-button:hover { background: var(--surface-hover); border: 0; }
.tab-button[aria-selected="true"] { background: var(--surface-layer); color: var(--accent-default); font-weight: 600; }
.tab-button[aria-selected="true"]::after {
position: absolute; inset: auto var(--space-3) 0 var(--space-3);
height: 2px; background: var(--accent-default); content: "";
}
.tab-button .task-dot {
display: none;
width: 7px; height: 7px; margin-left: 6px;
border-radius: 50%; background: var(--status-info);
vertical-align: middle;
}
.tab-button.has-task .task-dot { display: inline-block; }
.page-stage { min-height: 0; overflow: hidden; }
.view { display: none; height: 100%; min-height: 0; }
.view.active { display: flex; flex-direction: column; }
/* ============================================================
统一页面骨架
页头 → 数据准备区 → 表格命令区 → 表格 → 状态反馈区 → 完成任务区
============================================================ */
.page-header {
display: flex; align-items: center; gap: var(--space-3);
min-height: 48px; padding: var(--space-2) var(--space-4);
background: var(--surface-layer);
border-bottom: 1px solid var(--stroke-divider);
}
.page-title { margin: 0; font-size: 16px; font-weight: 600; }
.page-scope { color: var(--text-secondary); font-size: 12px; }
.page-header .push { margin-left: auto; }
.page-body {
min-height: 0; flex: 1;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
gap: var(--space-3);
padding: var(--space-3) var(--space-4) var(--space-4);
overflow: hidden;
}
.zone-prepare {
display: flex; align-items: center; flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
min-height: 44px;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
font-size: 13px;
}
.zone-prepare .push { margin-left: auto; }
.zone-prepare .filename { font-family: var(--font-mono); font-size: 12px; }
.inline-field { display: flex; align-items: center; gap: var(--space-2); }
.inline-field select { width: auto; min-width: 132px; }
.zone-tablecmd { display: flex; align-items: end; flex-wrap: wrap; gap: var(--space-2); }
.field { display: grid; gap: var(--space-1); min-width: 0; }
.field.w-sm { width: 128px; }
.field.w-md { width: 156px; }
.field.w-lg { width: 200px; }
.zone-tablecmd .result-count { margin-left: auto; color: var(--text-secondary); font-size: 12px; align-self: center; }
.table-shell {
min-height: 120px;
overflow: auto;
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
}
table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
th, td { padding: var(--space-2) var(--space-2); text-align: left; }
th {
position: sticky; top: 0; z-index: 1;
background: var(--surface-subtle);
border-bottom: 1px solid var(--stroke-control);
color: var(--text-secondary);
font-size: 12px; font-weight: 600;
white-space: nowrap;
}
th.sortable { cursor: pointer; user-select: none; }
th.sortable:hover { background: var(--surface-hover); color: var(--text-primary); }
th .sort-mark { opacity: 0; margin-left: 4px; }
th[aria-sort="ascending"] .sort-mark, th[aria-sort="descending"] .sort-mark { opacity: 1; }
th[aria-sort="descending"] .sort-mark { display: inline-block; transform: rotate(180deg); }
td {
height: 40px;
border-bottom: 1px solid var(--stroke-divider);
font-size: 13px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.col-check { width: 40px; }
.col-check td, th.col-check { text-align: center; padding-left: 0; padding-right: 0; }
tbody tr:hover { background: var(--surface-hover); }
/* 当前行、选择、焦点三种状态视觉可区分,且不只依赖颜色 */
tbody tr[aria-selected="true"] { background: var(--accent-subtle); }
tbody tr.current td:first-child { box-shadow: inset 3px 0 0 var(--accent-default); }
tbody tr:focus-visible { outline: 2px solid var(--focus-stroke); outline-offset: -2px; }
tbody tr:last-child td { border-bottom: 0; }
.muted { color: var(--text-secondary); }
.nowrap-id { font-family: var(--font-mono); font-size: 12px; }
.table-empty {
display: none;
padding: var(--space-6) var(--space-4);
text-align: center;
color: var(--text-secondary);
}
.table-empty strong { display: block; margin-bottom: var(--space-1); color: var(--text-primary); font-size: 14px; }
.table-shell.is-empty table { display: none; }
.table-shell.is-empty .table-empty { display: block; }
/* ---- 状态反馈区:选择工具栏 / 进度 / 信息条,插入位置稳定 ---- */
.zone-feedback { display: grid; gap: var(--space-2); }
.selection-bar {
display: none;
align-items: center; gap: var(--space-3);
min-height: 40px; padding: var(--space-1) var(--space-3);
border: 1px solid var(--accent-default);
border-radius: var(--radius-control);
background: var(--accent-subtle);
font-size: 13px;
}
.selection-bar.show { display: flex; }
.selection-bar .push { margin-left: auto; }
.progress-deck { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
.progress-row {
display: grid; grid-template-columns: auto minmax(80px, 1fr) auto;
align-items: center; gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
font-size: 12px;
}
.progress-track { height: 6px; overflow: hidden; border-radius: 3px; background: var(--surface-pressed); }
.progress-value { height: 100%; width: 0; border-radius: inherit; background: var(--accent-default); transition: width var(--motion-normal) var(--ease); }
.progress-value.done { background: var(--status-success); }
/* 不确定进度:总量未知时使用 */
.progress-track.indeterminate .progress-value {
width: 35% !important;
animation: indeterminate 1.4s ease-in-out infinite;
}
@keyframes indeterminate { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
.progress-meta { color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* ---- 信息条:页面级可恢复问题 / 部分成功,持久直到解决或关闭 ---- */
.infobar {
display: none;
align-items: flex-start; gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-left-width: 4px;
border-radius: var(--radius-control);
background: var(--surface-layer);
font-size: 13px;
}
.infobar.show { display: flex; }
.infobar .body { flex: 1; min-width: 0; }
.infobar .body strong { display: block; }
.infobar .body .detail { color: var(--text-secondary); font-size: 12px; }
.infobar .actions { display: flex; gap: var(--space-2); flex-shrink: 0; }
.infobar.caution { border-left-color: var(--status-caution); background: var(--status-caution-bg); }
.infobar.critical { border-left-color: var(--status-critical); background: var(--status-critical-bg); }
.infobar.info { border-left-color: var(--status-info); background: var(--status-info-bg); }
/* ---- 完成任务区 ---- */
.zone-complete {
display: flex; align-items: center; gap: var(--space-3);
flex-wrap: wrap;
min-height: 48px;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-control);
border-radius: var(--radius-control);
background: var(--surface-layer);
}
.zone-complete .scope-note { color: var(--text-secondary); font-size: 12px; }
.zone-complete .push { margin-left: auto; }
.zone-complete .actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* ---- 状态 chip:颜色 + 文本双通道 ---- */
.status-chip {
display: inline-flex; align-items: center; gap: 4px;
min-height: 22px; padding: 1px var(--space-2);
border: 1px solid transparent;
border-radius: 11px;
font-size: 12px; white-space: nowrap;
}
.chip-success { color: var(--status-success); background: var(--status-success-bg); border-color: currentColor; }
.chip-caution { color: var(--status-caution); background: var(--status-caution-bg); border-color: currentColor; }
.chip-critical { color: var(--status-critical); background: var(--status-critical-bg); border-color: currentColor; }
.chip-info { color: var(--status-info); background: var(--status-info-bg); border-color: currentColor; }
.chip-pending { color: var(--status-pending); background: var(--status-pending-bg); border-color: currentColor; }
.chip-muted { color: var(--status-muted); background: var(--status-muted-bg); border-color: currentColor; }
/* ============================================================
② AI生成:提示词侧面板(前置配置)
============================================================ */
.generate-layout { min-height: 0; flex: 1; display: grid; grid-template-columns: minmax(280px, .68fr) minmax(560px, 2fr); overflow: hidden; }
.prompt-pane {
display: flex; flex-direction: column; gap: var(--space-3);
min-height: 0; padding: var(--space-3) var(--space-4);
overflow: auto;
background: var(--surface-subtle);
border-right: 1px solid var(--stroke-divider);
}
.prompt-pane .page-body { padding: 0; }
.editor-group { display: grid; gap: var(--space-2); }
.group-head { display: flex; align-items: center; gap: var(--space-2); }
.group-head strong { font-size: 13px; }
.group-head .push { margin-left: auto; }
.editor-toolbar { display: flex; gap: var(--space-2); }
.editor-toolbar select { flex: 1; min-width: 0; }
.generate-main { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; gap: var(--space-3); padding: var(--space-3) var(--space-4) var(--space-4); overflow: hidden; }
.run-log {
max-height: 72px; overflow: auto;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--stroke-divider);
border-radius: var(--radius-control);
background: var(--surface-subtle);
color: var(--text-secondary);
font-family: var(--font-mono); font-size: 12px; line-height: 1.6;
}
/* ============================================================
列表/详情页(账号管理、商品套图)
============================================================ */
.split-layout { min-height: 0; flex: 1; display: grid; grid-template-columns: minmax(230px, .7fr) minmax(420px, 2fr); gap: var(--space-3); padding: var(--space-3) var(--space-4) var(--space-4); overflow: hidden; }
.side-list { min-height: 0; overflow: auto; padding: var(--space-1); border: 1px solid var(--stroke-control); border-radius: var(--radius-control); background: var(--surface-layer); }
.list-item {
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: var(--space-2);
width: 100%; min-height: 48px; padding: var(--space-2) var(--space-2);
border: 0; border-bottom: 1px solid var(--stroke-divider); border-radius: 0;
background: transparent; text-align: left;
}
.list-item:last-child { border-bottom: 0; }
.list-item[aria-current="true"] { background: var(--accent-subtle); box-shadow: inset 3px 0 0 var(--accent-default); }
.list-main { min-width: 0; }
.list-title { font-weight: 600; font-size: 13px; }
.list-meta { overflow: hidden; color: var(--text-secondary); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.detail-pane { min-height: 0; overflow: auto; border: 1px solid var(--stroke-control); border-radius: var(--radius-control); background: var(--surface-layer); }
.form-section { padding: var(--space-4); border-bottom: 1px solid var(--stroke-divider); }
.form-section:last-child { border-bottom: 0; }
.form-section h2 { margin: 0 0 var(--space-3); font-size: 13px; font-weight: 600; }
.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">cmhub 已连接</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">调用 cmhub 生成新标题与新封面,会消耗点数</span>
<span class="push"></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>
<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 张原图">AI 帮写</button>
<button class="btn-primary" type="button" data-open="suite-confirm-dialog">生成套图</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>
<div class="form-section">
<h2>cmhub 网关</h2>
<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="cmhub_api_key_placeholder"><span class="field-hint">保存在本地配置,不会写入日志。</span></div>
<div class="field"><label for="set-text-model">生文别名</label><select id="set-text-model"><option>text-standard</option></select></div>
<div class="field"><label for="set-image-model">生图别名</label><select id="set-image-model"><option>image-hd</option></select></div>
<div class="field"><label for="set-vision-model">图片理解别名</label><select id="set-vision-model"><option>vision-standard</option></select></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 class="field"><label for="set-connect">连接超时秒数</label><input type="text" id="set-connect" value="66"></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-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>
</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); });
settingsSave.addEventListener('click', function () { setDirty(false); showToast('设置已保存'); });
settingsDiscard.addEventListener('click', function () { setDirty(false); showToast('已放弃未保存的更改'); });
document.querySelectorAll('[data-unsaved]').forEach(function (button) {
button.addEventListener('click', function () {
button.closest('dialog').close();
if (button.dataset.unsaved === 'save') { setDirty(false); showToast('设置已保存'); }
else { setDirty(false); showToast('已放弃未保存的更改'); }
});
});
/* ---------- 主题切换(用于评审浅色/深色,非产品功能) ---------- */
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>