feat(prototype): show record details inline

This commit is contained in:
QiuSW
2026-08-03 15:55:11 +08:00
parent 0b757f8ad1
commit 79a2767870
6 changed files with 203 additions and 74 deletions
+1 -1
View File
@@ -26,7 +26,7 @@
| T-003 | 建立 `init.ps1` 统一入口 | T-001, T-002 | 一条命令完成两端安装与基础验证并打印启动命令;未配置时主动失败而不是静默跳过 |
| T-004 | 建立核心数据模型与状态机 | T-001 | `tasks`、`order_authorizations`、`order_submissions` 表与 `04-architecture.md` 一致;状态流转有单元测试;金额用十进制字符串 |
| T-005 | 网页端 MVP 交互原型 | - | `docs/design/web-*.html` 单文件假数据原型覆盖登录、表格工作台、创建弹窗、批量开始试选、路由化详情抽屉与完整详情;键盘、窄屏、空态 / 错误 / 加载、围栏后调和状态可演示;经人工确认前保持 `DOING` |
| T-006 | 桌面端 MVP 交互原型 | - | `docs/design/desk-*.html` 单文件假数据原型覆盖“采购工具”的采购执行 / 配置固定页签、顶部轮询控制、当前任务图片、滚动日志、时间倒序执行记录和详情弹窗;明确 dry-run / 真实下单、App 版本失配、围栏后不可重试;经人工确认前保持 `DOING` |
| T-006 | 桌面端 MVP 交互原型 | - | `docs/design/desk-*.html` 单文件假数据原型覆盖“采购工具”的采购执行 / 配置固定页签、顶部轮询控制、当前任务图片、滚动日志、时间倒序执行记录,以及左侧当前任务 / 记录详情双视图;明确 dry-run / 真实下单、App 版本失配、围栏后不可重试;经人工确认前保持 `DOING` |
### Phase 1 · 真机取证(最高风险,必须先做)
+5 -4
View File
@@ -110,7 +110,7 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。
- 关联用户故事:US-003、US-008 | 关联需求:F-005、F-011
- 页面 / 组件:desk 采购执行页 `PollControls` + `DeviceStatusBar` + `CurrentTaskPanel` +
`ExecutionLog` + `ExecutionRecordTable` / `ExecutionRecordDialog`
`ExecutionLog` + `ExecutionRecordTable` + `ExecutionRecordDetailPanel`
- 目标角色:采购执行员
- 前置条件:web 端连接正常;设备已通过连接检查
@@ -119,8 +119,8 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。
1. 用户确认 web、设备和已取证 App 版本三项状态都就绪,在顶部状态区点击「开始轮询」。
2. 每到间隔时间领取一条任务,界面显示**这一趟是试选还是下单**、商品、规格、当前步骤。
3. 第一趟试选完成后**退出商品页释放手机**,继续下一轮轮询。
4. 执行结果按时间倒序进入右侧两列表格;单击只选中,双击、Enter 或可见查看命令打开记录
详情,关闭后焦点回到原行。
4. 执行结果按时间倒序进入右侧两列表格;单击只选中,双击、Enter 或可见查看命令在左侧打开
记录详情。宽屏可继续选择其他记录;可见返回按钮或 `Esc` 回到当前任务并恢复当前行焦点。
**状态与异常清单**
@@ -130,7 +130,8 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。
| 轮询中 | 显示下次轮询倒计时与连续失败计数 |
| 执行中 | 显示当前趟次与步骤;**真机步骤期间禁用硬取消和关闭窗口** |
| 当前任务图片 | 无可信图片时显示空态;有图时标来源与采集时间,不用其他来源图片凑合 |
| 执行记录详情 | 左侧原始文字、右侧图片证据、下方结构化采购结果;不提供下单、重试提交或付款动作 |
| 执行记录详情 | 左栏在当前任务 / 记录详情间切换,不打开模态框;详情内左侧原始文字、右侧图片证据、下方结构化采购结果;顶部实时状态不中断;不提供下单、重试提交或付款动作 |
| 详情读取异常 | 加载、失败、无图和记录不存在分别说明;仅可重试读取同一稳定记录 ID,过期异步结果不得覆盖当前视图 |
| 无可领任务 | 显示「暂无待领任务」并继续下一轮,**不当作错误、不计入失败计数** |
| 待人工 | 整页显著变色,说明缺什么、下一步做什么;该任务不再被本端领取 |
| 连续失败 | 达到阈值自动停止轮询并显示原因,**不无限重试** |
+6 -5
View File
@@ -22,7 +22,7 @@
- 标准启动路径:`./init.ps1`(Windows)/ `./init.sh`。已存在但会因两端目录未初始化而以
退出码 3 结束并列出待办任务——这是预期行为
- 标准验证路径:目前只有 `python scripts/validate_agent_context.py` 可跑通
- 当前 blocker:T-005 第二轮 Web 表格工作台和 T-006 第二轮“采购工具”桌面工作区均已通过
- 当前 blocker:T-005 第二轮 Web 表格工作台和 T-006 第三轮“采购工具”桌面工作区均已通过
自动检查;两者经人工确认前不开始对应生产 UI。
## 当前目录要点
@@ -43,11 +43,12 @@
- 已完成:无。
- 正在进行:T-005(网页端 MVP 交互原型)、T-006(桌面端 MVP 交互原型)。T-005 第二轮
已将三泳道改为传统表格;T-006 第二轮已改为“采购工具”的采购执行 / 配置固定页签,并增加
顶部轮询控制、当前任务图片、滚动日志、执行记录表格和详情弹窗。两项自动验证均已完成,
已将三泳道改为传统表格;T-006 第三轮已把执行记录详情从模态框改为左侧内联双视图,支持
连续选行、可见返回按钮、`Esc` 层级、异常读取状态和紧凑宽度单视图。两项自动验证均已完成,
都因 `needs_human_review: true` 保持 `DOING`。
- **下一步:人工浏览 6 个原型,重点确认 Web 表格与详情抽屉,以及桌面端顶部状态区、三块
布局、图片证据、两列执行记录和详情弹窗。** 明确确认后才能把 T-005 / T-006 改为 `DONE`。
- **下一步:人工浏览 6 个原型,重点确认 Web 表格与详情抽屉,以及桌面端顶部状态区、图片
证据、两列执行记录和左侧内联详情 / 返回路径。** 明确确认后才能把 T-005 / T-006 改为
`DONE`。
- 原型使用假数据、不调用真实接口、不驱动真机;T-001 / T-002 骨架可在确认后并行,
Phase 2 生产页面还必须等待相关真机结论。
+142 -52
View File
@@ -168,7 +168,7 @@
main { max-width: 1480px; margin: 0 auto; padding: 16px; }
.prototype-tools {
display: grid;
grid-template-columns: minmax(240px, 1fr) auto;
grid-template-columns: minmax(240px, 1fr) minmax(180px, .55fr) auto;
gap: 12px;
align-items: end;
margin-bottom: 12px;
@@ -224,7 +224,9 @@
.banner p { margin: 0; color: var(--text-primary); }
.banner button[hidden] { display: none; }
.workspace { display: grid; grid-template-columns: minmax(0, 1.85fr) minmax(320px, 1fr); gap: 12px; margin-top: 12px; align-items: stretch; }
.left-pane { min-width: 0; min-height: 0; }
.stack { display: grid; grid-template-rows: auto minmax(240px, 1fr); gap: 12px; min-width: 0; }
.stack[hidden], .detail-workspace[hidden] { display: none; }
.card { border: 1px solid var(--stroke); border-radius: var(--radius-card); background: var(--surface-raised); box-shadow: 0 2px 8px rgba(0,0,0,.04); overflow: hidden; }
.card-header { display: flex; align-items: start; justify-content: space-between; gap: 12px; padding: 14px 16px 10px; border-bottom: 1px solid var(--divider); }
.card-header h1, .card-header h2 { margin: 0; font-size: 17px; line-height: 1.35; }
@@ -296,11 +298,17 @@
.dialog-body p { margin: 0 0 10px; }
.dialog-body code { overflow-wrap: anywhere; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--divider); }
.record-dialog { width: min(940px, calc(100vw - 32px)); }
.record-dialog header { display: flex; justify-content: space-between; gap: 12px; padding: 16px 20px 12px; border-bottom: 1px solid var(--divider); }
.record-dialog header h2 { margin: 0; font-size: 18px; }
.record-dialog header p { margin: 3px 0 0; color: var(--text-secondary); font-size: 12px; }
.record-dialog-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr); gap: 16px; padding: 16px 20px; }
.detail-workspace { min-height: 540px; height: 100%; display: flex; flex-direction: column; }
.detail-header { align-items: center; }
.detail-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
.shortcut { margin-left: 5px; padding: 1px 5px; border: 1px solid var(--stroke); border-bottom-width: 2px; border-radius: 3px; color: var(--text-secondary); background: var(--surface-muted); font-size: 11px; }
.detail-scroll { min-height: 0; overflow: auto; flex: 1; }
.record-detail-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(240px, 2fr); gap: 16px; padding: 16px 20px; }
.detail-feedback { margin: 16px 20px 0; padding: 16px; border: 1px solid var(--stroke); border-radius: 6px; background: var(--surface-muted); }
.detail-feedback[hidden] { display: none; }
.detail-feedback strong, .detail-feedback span { display: block; }
.detail-feedback span { margin-top: 4px; color: var(--text-secondary); }
.detail-feedback button { margin-top: 12px; }
.detail-panel { min-width: 0; }
.detail-panel h3, .record-result h3 { margin: 0 0 9px; font-size: 14px; }
.detail-list { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 7px 12px; margin: 0; }
@@ -322,6 +330,7 @@
.workspace { grid-template-columns: 1fr; }
.stack { grid-template-rows: auto auto; }
.record-card { min-height: 480px; }
.workspace[data-mode="record"] .record-card { display: none; }
.log-list { max-height: 220px; }
}
@media (max-width: 640px) {
@@ -346,7 +355,9 @@
.log-tools, .record-actions { justify-content: flex-start; }
.log-list li { grid-template-columns: 58px 1fr; }
.log-list li span:last-child { grid-column: 1 / -1; }
.record-dialog-grid { grid-template-columns: 1fr; }
.record-detail-grid { grid-template-columns: 1fr; }
.detail-header-actions { align-items: stretch; justify-content: flex-start; }
.detail-header-actions .badge { white-space: normal; }
.evidence-grid { grid-template-columns: 1fr; }
.detail-list { grid-template-columns: 1fr; gap: 2px; }
.detail-list dd { margin-bottom: 7px; }
@@ -403,6 +414,14 @@
<option value="failed_stop">连续失败 · 轮询已停止</option>
</select>
</label>
<label for="detail-state">记录详情状态(仅演示读取反馈)
<select id="detail-state">
<option value="normal" selected>正常</option>
<option value="loading">加载中</option>
<option value="error">加载失败</option>
<option value="missing">记录不存在</option>
</select>
</label>
<div class="tool-buttons">
<button type="button" id="reset">复位假数据</button>
<button type="button" id="close-demo">演示关闭窗口</button>
@@ -433,8 +452,9 @@
<button type="button" id="context-action" hidden>查看说明</button>
</section>
<div class="workspace">
<div class="stack">
<div class="workspace" id="workspace" data-mode="live">
<div class="left-pane">
<div class="stack" id="live-workspace">
<section class="card" aria-labelledby="task-title">
<div class="card-header">
<div><h1 id="task-title">当前任务</h1><p id="task-subtitle">尚未领取任务</p></div>
@@ -482,6 +502,47 @@
</div>
<ul class="log-list" id="logs" aria-live="polite" aria-relevant="additions"></ul>
</section>
</div>
<section class="card detail-workspace" id="record-detail-pane" aria-labelledby="record-detail-title" aria-busy="false" hidden>
<div class="card-header detail-header">
<div><h1 id="record-detail-title">执行记录详情</h1><p id="record-detail-meta">选择右侧记录查看历史快照</p></div>
<div class="detail-header-actions">
<span class="badge" id="live-task-state" data-tone="success" aria-live="polite">实时:未启动</span>
<button type="button" id="return-live">返回当前任务 <span class="shortcut" aria-hidden="true">Esc</span></button>
</div>
</div>
<div class="detail-feedback" id="detail-feedback" role="status" aria-live="polite" hidden>
<strong id="detail-feedback-title"></strong>
<span id="detail-feedback-text"></span>
<button type="button" id="retry-detail" hidden>重新读取记录</button>
</div>
<div class="detail-scroll" id="detail-content">
<div class="record-detail-grid">
<section class="detail-panel" aria-labelledby="original-copy-title">
<h3 id="original-copy-title">原始文字</h3>
<dl class="detail-list">
<dt>任务编号</dt><dd id="detail-task-id"></dd>
<dt>商品标题</dt><dd id="detail-product"></dd>
<dt>目标规格</dt><dd id="detail-spec"></dd>
<dt>金额边界</dt><dd id="detail-cap"></dd>
<dt>提交围栏</dt><dd id="detail-submission"></dd>
<dt>原始说明</dt><dd id="detail-note"></dd>
</dl>
<ol class="timeline" id="detail-timeline" aria-label="执行时间线"></ol>
</section>
<section class="detail-panel" aria-labelledby="evidence-title">
<h3 id="evidence-title">图片证据</h3>
<div class="evidence-grid" id="detail-evidence"></div>
</section>
</div>
<section class="record-result" aria-labelledby="result-title">
<h3 id="result-title">采购结果</h3>
<p class="result-value" id="detail-result"></p>
<p class="result-note" id="detail-result-note"></p>
</section>
</div>
</section>
</div>
<section class="card record-card" aria-labelledby="record-title">
@@ -496,7 +557,7 @@
</table>
<div class="table-empty" id="record-empty" hidden>暂无执行记录。开始轮询并产生结果后会显示在这里。</div>
</div>
<p class="table-help" id="record-help">单击选择;双击行、按 Enter 或使用“查看所选记录”打开详情。右键也可查看。</p>
<p class="table-help" id="record-help">单击选择;双击行、按 Enter 或使用“查看所选记录”在左侧打开详情。详情态可连续选择其他记录。</p>
</section>
</div>
</main>
@@ -505,37 +566,6 @@
<button type="button" id="record-menu-open" role="menuitem">查看记录详情</button>
</div>
<dialog class="record-dialog" id="record-dialog" aria-labelledby="record-dialog-title">
<header>
<div><h2 id="record-dialog-title">执行记录详情</h2><p id="record-dialog-meta"></p></div>
<button type="button" id="record-dialog-close" aria-label="关闭执行记录详情">关闭</button>
</header>
<div class="record-dialog-grid">
<section class="detail-panel" aria-labelledby="original-copy-title">
<h3 id="original-copy-title">原始文字</h3>
<dl class="detail-list">
<dt>任务编号</dt><dd id="detail-task-id"></dd>
<dt>商品标题</dt><dd id="detail-product"></dd>
<dt>目标规格</dt><dd id="detail-spec"></dd>
<dt>金额边界</dt><dd id="detail-cap"></dd>
<dt>提交围栏</dt><dd id="detail-submission"></dd>
<dt>原始说明</dt><dd id="detail-note"></dd>
</dl>
<ol class="timeline" id="detail-timeline" aria-label="执行时间线"></ol>
</section>
<section class="detail-panel" aria-labelledby="evidence-title">
<h3 id="evidence-title">图片证据</h3>
<div class="evidence-grid" id="detail-evidence"></div>
</section>
</div>
<section class="record-result" aria-labelledby="result-title">
<h3 id="result-title">采购结果</h3>
<p class="result-value" id="detail-result"></p>
<p class="result-note" id="detail-result-note"></p>
</section>
<div class="dialog-actions"><button type="button" class="primary" id="record-dialog-done">关闭详情</button></div>
</dialog>
<dialog id="info-dialog" aria-labelledby="dialog-title">
<div class="dialog-body">
<h2 id="dialog-title">安全说明</h2>
@@ -593,6 +623,8 @@
};
let currentRecordId = records[0]?.id || null;
let recordFocusReturn = null;
let workspaceMode = 'live';
let detailRequestId = 0;
const baseSteps = {
idle: [],
trial: ['打开商品(待真机判据)', '精确匹配颜色分类与尺码', '从规格面板读取单价', '截图并退出商品页'],
@@ -671,13 +703,19 @@
}
function selectRecord(id, focus = false) {
currentRecordId = id;
let selectedRow = null;
for (const row of $('record-rows').rows) {
const selected = row.dataset.recordId === id;
row.setAttribute('aria-selected', String(selected));
row.tabIndex = selected ? 0 : -1;
if (selected) selectedRow = row;
if (selected && focus) row.focus();
}
$('view-record').disabled = !records.some((record) => record.id === id);
if (workspaceMode === 'record') {
recordFocusReturn = selectedRow || recordFocusReturn;
renderRecordDetail(id);
}
}
function renderRecords() {
const rows = records.map((record) => {
@@ -724,19 +762,65 @@
const caption = document.createElement('figcaption'); caption.textContent = `${label} · 不访问真实拼多多`;
figure.append(svg, caption); return figure;
}
function openRecord(id, source) {
const record = records.find((item) => item.id === id); if (!record) return;
hideRecordMenu(); currentRecordId = id; recordFocusReturn = source || [...$('record-rows').rows].find((row) => row.dataset.recordId === id) || $('view-record');
function showDetailFeedback(title, text, retry = false) {
$('detail-feedback-title').textContent = title;
$('detail-feedback-text').textContent = text;
$('retry-detail').hidden = !retry;
$('detail-feedback').hidden = false;
$('detail-content').hidden = true;
}
async function renderRecordDetail(id) {
const requestId = ++detailRequestId;
const prototypeState = $('detail-state').value;
const record = records.find((item) => item.id === id);
$('record-detail-pane').setAttribute('aria-busy', String(prototypeState === 'loading' || prototypeState === 'normal'));
if (prototypeState === 'loading') {
showDetailFeedback('正在读取执行记录', `正在按稳定记录 ID ${id} 读取文字、图片索引和采购结果。`);
return;
}
if (prototypeState === 'error') {
showDetailFeedback('执行记录加载失败', '当前任务与右侧列表不受影响。可安全重试读取同一记录,不会重新执行采购。', true);
$('record-detail-pane').setAttribute('aria-busy', 'false');
return;
}
if (prototypeState === 'missing' || !record) {
showDetailFeedback('执行记录已不存在', '记录可能已归档或当前账号无权查看。请返回当前任务;不会用其他记录补齐内容。');
$('record-detail-pane').setAttribute('aria-busy', 'false');
return;
}
showDetailFeedback('正在读取执行记录', `正在加载 ${record.taskId} 的已保存证据。`);
await Promise.resolve();
if (requestId !== detailRequestId || workspaceMode !== 'record' || currentRecordId !== id) return;
const status = recordStatus[record.status] || {label:record.status};
$('record-dialog-title').textContent = record.title;
$('record-dialog-meta').textContent = `${record.taskId} · ${formatRecordTime(record.collectedAt)} · ${status.label}`;
$('record-detail-title').textContent = record.title;
$('record-detail-meta').textContent = `${record.taskId} · ${formatRecordTime(record.collectedAt)} · ${status.label}`;
$('detail-task-id').textContent = record.taskId; $('detail-product').textContent = record.title; $('detail-spec').textContent = record.spec;
$('detail-cap').textContent = record.cap; $('detail-submission').textContent = record.submission; $('detail-note').textContent = record.note;
$('detail-timeline').replaceChildren(...record.timeline.map((value) => { const li = document.createElement('li'); li.textContent = value; return li; }));
if (record.evidence.length) $('detail-evidence').replaceChildren(...record.evidence.map(createEvidence));
else { const empty = document.createElement('div'); empty.className = 'preview-empty'; const strong = document.createElement('strong'); strong.textContent = '暂无可信图片'; empty.append(strong, '记录保留原始文字,不用其他图片凑合。'); $('detail-evidence').replaceChildren(empty); }
$('detail-result').textContent = record.result; $('detail-result-note').textContent = record.resultNote;
$('record-dialog').showModal();
$('detail-feedback').hidden = true; $('detail-content').hidden = false;
$('record-detail-pane').setAttribute('aria-busy', 'false');
}
function openRecord(id, source) {
const record = records.find((item) => item.id === id); if (!record) return;
hideRecordMenu(); currentRecordId = id;
recordFocusReturn = source || [...$('record-rows').rows].find((row) => row.dataset.recordId === id) || $('view-record');
workspaceMode = 'record'; $('workspace').dataset.mode = 'record';
$('live-workspace').hidden = true; $('record-detail-pane').hidden = false;
renderRecordDetail(id);
$('return-live').focus();
$('announcer').textContent = `已在左侧打开 ${record.title} 的执行记录详情。按 Escape 或返回当前任务按钮返回。`;
}
function showLiveWorkspace() {
if (workspaceMode !== 'record') return;
detailRequestId += 1; workspaceMode = 'live'; $('workspace').dataset.mode = 'live';
$('record-detail-pane').hidden = true; $('live-workspace').hidden = false;
const currentRow = [...$('record-rows').rows].find((row) => row.dataset.recordId === currentRecordId);
const focusTarget = currentRow || (recordFocusReturn?.isConnected ? recordFocusReturn : $('view-record'));
focusTarget?.focus();
$('announcer').textContent = '已返回当前任务;执行记录选择和列表位置已保留。';
}
function render(key, announce = true) {
const s = scenarios[key];
@@ -760,6 +844,7 @@
renderSteps(s.kind, s.active);
$('countdown').textContent = s.countdown; $('failure-count').textContent = s.fail; $('completed-count').textContent = String(s.count || 0);
$('session-state').textContent = s.session; $('current-step').textContent = s.step;
$('live-task-state').textContent = `实时:${s.session} · ${s.step}`; $('live-task-state').dataset.tone = s.tone;
$('primary-action').textContent = sessionControl.label; $('primary-action').disabled = !sessionControl.enabled;
$('action-reason').textContent = sessionControl.reason;
$('context-action').hidden = !s.safeAction; $('context-action').textContent = s.safeAction ? s.action : '';
@@ -777,7 +862,7 @@
$('scenario').addEventListener('change', (e) => render(e.target.value));
$('theme').addEventListener('change', (e) => { document.documentElement.dataset.theme = e.target.value; $('announcer').textContent = `主题已切换为${e.target.options[e.target.selectedIndex].text}`; });
$('reset').addEventListener('click', () => { $('scenario').value = 'idle'; $('logs').replaceChildren(); currentRecordId = records[0]?.id || null; renderRecords(); render('idle'); appendLog('状态', '假数据已复位。'); });
$('reset').addEventListener('click', () => { showLiveWorkspace(); $('scenario').value = 'idle'; $('detail-state').value = 'normal'; $('logs').replaceChildren(); currentRecordId = records[0]?.id || null; renderRecords(); render('idle'); appendLog('状态', '假数据已复位。'); });
$('primary-action').addEventListener('click', () => {
const next = $('scenario').value === 'idle' ? 'polling' : 'idle'; $('scenario').value = next; render(next);
});
@@ -790,11 +875,16 @@
});
$('view-record').addEventListener('click', () => openRecord(currentRecordId, [...$('record-rows').rows].find((row) => row.dataset.recordId === currentRecordId)));
$('record-menu-open').addEventListener('click', () => openRecord(currentRecordId, [...$('record-rows').rows].find((row) => row.dataset.recordId === currentRecordId)));
$('record-dialog-close').addEventListener('click', () => $('record-dialog').close());
$('record-dialog-done').addEventListener('click', () => $('record-dialog').close());
$('record-dialog').addEventListener('close', () => { if (recordFocusReturn?.isConnected) recordFocusReturn.focus(); });
$('return-live').addEventListener('click', showLiveWorkspace);
$('detail-state').addEventListener('change', () => { if (workspaceMode === 'record') renderRecordDetail(currentRecordId); });
$('retry-detail').addEventListener('click', () => { $('detail-state').value = 'normal'; renderRecordDetail(currentRecordId); });
document.addEventListener('pointerdown', (event) => { if (!$('record-menu').hidden && !$('record-menu').contains(event.target)) hideRecordMenu(); });
document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !$('record-menu').hidden) hideRecordMenu(); });
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
if (!$('record-menu').hidden) { event.preventDefault(); hideRecordMenu(); return; }
if ($('info-dialog').open) return;
if (workspaceMode === 'record') { event.preventDefault(); showLiveWorkspace(); }
});
window.addEventListener('resize', hideRecordMenu);
$('export-log').addEventListener('click', exportDemo);
$('close-demo').addEventListener('click', () => {
+7 -5
View File
@@ -96,14 +96,16 @@
- 顶部状态区:web、设备、拼多多 App 实际 / 已取证版本和会话状态;开始 / 停止轮询固定在
最右侧且只控制轮询会话。版本不一致时 fail closed 并提示重新取证。
- 宽屏三块工作区:左上当前任务,左下滚动日志,右侧执行记录;窄屏按此顺序改为单列。
- 宽屏是主从工作区:左侧在“当前任务 + 滚动日志”和“执行记录详情”之间切换,右侧执行记录
保持可见;窄屏进入详情时暂时收起记录表,返回后恢复列表现场。
- 当前任务左侧显示商品、规格、数量、**当前是第一趟还是第二趟**、步骤和剩余租约;右侧约
1/3 显示可选商品 / 证据预览。无可信图片时保持空态,有图必须标来源与采集时间。
- 滚动日志标题区同时显示下次轮询、连续失败和本次完成,不再使用独立会话卡或底部控制条。
- 执行记录只显示标题、状态两列,任务编号与时间放在标题次行,按采集时间倒序。单击选择;
双击、Enter、可见“查看所选记录”或右键打开详情。
- 记录详情上部左侧显示原始文字、右侧显示图片证据,下部显示结构化采购结果;关闭后焦点
返回原行。图片缺失不使用其他来源凑合。
双击、Enter、可见“查看所选记录”或右键在左侧打开详情;详情态单击另一行直接更新详情。
- 记录详情不是模态框:上部左侧显示原始文字、右侧显示图片证据,下部显示结构化采购结果;
可见“返回当前任务”与 `Esc` 均能返回并恢复当前行焦点。加载失败、无图和记录不存在有明确
状态,图片缺失不使用其他来源凑合。
- **待人工时整页显著变色并说明缺什么**,不要让执行员盯着一个静止画面猜。
- 关闭窗口即停止轮询;连续失败达阈值自动停止并显示原因。
@@ -142,7 +144,7 @@
| `CurrentTaskPanel` | desk 采购执行页 | 当前任务文字、步骤与带来源的可选图片证据 |
| `ExecutionLog` | desk 采购执行页 | 最新在上的滚动日志与假诊断导出 |
| `ExecutionRecordTable` | desk 采购执行页 | 标题 / 状态两列、时间倒序和稳定记录 ID |
| `ExecutionRecordDialog` | desk 采购执行页 | 原始文字、图片证据、采购结果与焦点恢复 |
| `ExecutionRecordDetailPanel` | desk 采购执行页 | 左侧内联原始文字、图片证据、采购结果、读取状态与返回焦点 |
## 六、原型
+42 -7
View File
@@ -168,14 +168,49 @@ desk 端将使用 PySide6 / Qt Widgets,但当前没有可确认的桌面应用
## 执行记录
### 2026-08-03 · 第三轮内联详情待实现
### 2026-08-03 · 第三轮左侧内联详情实现
- 任务保持 `DOING`。本轮拟将执行记录详情从模态对话框改为左侧双视图工作区;只修改 T-006
写路径内的原型与同步文档,不扩展 API、真机或生产 PySide6 范围。
- 计划验证双击 / Enter / 可见命令 / 右键入口、详情态连续选行、返回按钮、`Esc` 层级、焦点
恢复、紧凑宽度单视图,以及加载 / 失败 / 缺图 / 记录不存在状态。
- PySide6 交接将把左栏映射为 `QStackedWidget`,整体映射为 `QSplitter`,右侧继续使用
`QTableView` + 稳定 ID 模型;详情异步结果必须带请求代次并在 GUI 线程丢弃过期回调。
- 任务继续保持 `DOING`:第三轮自动验证已经完成,但 `needs_human_review: true`,仍需人确认
工作区切换、返回路径、宽屏连续查看与紧凑窗口取舍。
- 执行记录详情已从模态 `dialog` 移到左侧双视图工作区。默认 `LIVE_TASK` 保持当前任务和滚动
日志;双击、Enter、可见命令或右键进入 `RECORD_DETAIL(record_id)`,右侧宽屏列表不被遮挡。
- 详情态单击或方向键选择另一行会按稳定记录 ID 更新左侧详情。可见“返回当前任务”按钮和
`Esc` 共用返回命令,保留列表选择 / 滚动,并把焦点还给当前记录行。
- `Esc` 先关闭右键菜单;信息对话框打开时不触发工作区返回。顶部连接、会话和安全 Banner
始终保留,详情头部另显示实时会话摘要,证明查看历史不会冻结当前任务状态表达。
- 原型控制器可演示详情正常、加载中、加载失败和记录不存在;缺图记录仍显示可信证据空态。
重新读取只读取同一记录,不执行采购。渲染带请求代次,切换记录或返回后丢弃旧异步结果。
- `windows-ui-ux` 使本轮采用非模态主从工作区、可见返回命令、分层 Escape、稳定选择与焦点
恢复,并明确紧凑宽度采用单视图,而不是把两个高密度面板纵向无限堆叠。
**第三轮 HTML → PySide6 / Qt Widgets 交接增量**
| 原型项 | 核心交互契约 | 推荐 Qt Widgets 映射 | 状态 / 线程要求 |
| --- | --- | --- | --- |
| 左侧双视图 | 当前任务 / 记录详情互斥;顶部状态不在切换区 | 左栏 `QStackedWidget`,整体 `QSplitter` | 页面切换不销毁当前任务模型、日志或列表状态 |
| 详情入口与连续选行 | 双击、Enter、按钮、右键共用命令;详情态换行立即换详情 | 共享 `QAction`;`QTableView.activated` 与按钮触发 | 记录身份使用模型中的稳定 ID,不使用代理行号 |
| 返回当前任务 | 可见按钮与 Escape 共用;恢复当前行焦点 | 共享返回 `QAction` + `QShortcut(Escape)` | 仅在无更高层浮层时启用;按稳定 ID 重新定位代理行 |
| 详情读取 | 加载 / 失败 / 缺图 / 不存在状态明确 | 状态页或 `QStackedWidget` + 只读详情控件 | worker 结果携带 record ID 与 request ID;GUI 线程拒绝过期结果 |
| 紧凑宽度 | 详情态暂时隐藏记录表,返回后恢复现场 | 响应窗口宽度调整 `QSplitter` 可见性 | 隐藏不重建模型,不丢选择、排序或滚动位置 |
**第三轮已运行验证**
- Node 静态检查:执行页 78 个 id 无重复,内联脚本可编译,执行记录详情 `dialog` 已不存在。
- Playwright 在 1440 px 验证双击、Enter、可见按钮 / 右键共用内联详情;详情态连续选行会更新
标题,返回按钮与 `Esc` 恢复当前记录行焦点;右键菜单和信息对话框的 Escape 层级正确。
- 详情正常、加载中、加载失败 / 重新读取、记录不存在、无图片均可演示;查看详情期间切换到
第一趟试选,顶部 Banner 和详情实时徽标继续更新,且详情不退出。
- 375 / 768 / 1024 / 1440 px 均无页面级水平溢出;375 / 768 px 详情态隐藏记录表,返回后
恢复表格与唯一选择;1024 / 1440 px 保持宽屏主从布局。浏览器 0 error / 0 warning,页面无
外部资源请求。
- `python scripts/validate_agent_context.py` 与 `git diff --check`:提交前运行并记录最终结果。
**第三轮未验证与待人工确认**
- 没有生产 PySide6、Model/View、worker、图片持久化、API、ADB 或真机行为;浏览器原型不能
证明原生 Qt 焦点、Narrator、UI Automation、DPI、线程或性能。
- 待人确认:宽屏查看详情时是否继续保留右表;紧凑宽度是否接受单视图;返回按钮位置、实时
状态徽标密度,以及详情加载 / 不存在文案。人工确认前任务不得改为 `DONE`。
### 2026-08-03 · 第二轮采购工具工作区实现