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
+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', () => {