Files
cmbuyer/docs/design/desk-execution.html
T

902 lines
66 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="zh-CN" data-theme="system">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>采购工具 · 采购执行原型</title>
<style>
:root {
color-scheme: light dark;
--surface-base: #f3f3f3;
--surface-layer: rgba(255, 255, 255, .86);
--surface-raised: #ffffff;
--surface-muted: #f8f8f8;
--text-primary: #1b1b1b;
--text-secondary: #5d5d5d;
--text-disabled: #8a8a8a;
--accent: #005fb8;
--accent-hover: #00549f;
--accent-pressed: #004578;
--accent-soft: #e7f3ff;
--success: #0f6b38;
--success-soft: #e7f5ec;
--caution: #8a5600;
--caution-soft: #fff4ce;
--critical: #b10e1e;
--critical-soft: #fde7e9;
--info: #005fb8;
--info-soft: #e7f3ff;
--stroke: #d0d0d0;
--divider: #e5e5e5;
--focus: #000000;
--shadow: 0 8px 24px rgba(0, 0, 0, .08);
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--radius-control: 4px;
--radius-card: 8px;
--motion-fast: 120ms;
}
html[data-theme="dark"] {
--surface-base: #202020;
--surface-layer: rgba(44, 44, 44, .94);
--surface-raised: #2b2b2b;
--surface-muted: #262626;
--text-primary: #f5f5f5;
--text-secondary: #c7c7c7;
--text-disabled: #8f8f8f;
--accent: #60aef2;
--accent-hover: #79bcf5;
--accent-pressed: #4798de;
--accent-soft: #12324b;
--success: #6ccb8e;
--success-soft: #173d27;
--caution: #f3c45b;
--caution-soft: #493711;
--critical: #ff99a4;
--critical-soft: #4e1d22;
--info: #75b9f3;
--info-soft: #12324b;
--stroke: #5c5c5c;
--divider: #3c3c3c;
--focus: #ffffff;
--shadow: 0 8px 24px rgba(0, 0, 0, .32);
}
@media (prefers-color-scheme: dark) {
html[data-theme="system"] {
--surface-base: #202020;
--surface-layer: rgba(44, 44, 44, .94);
--surface-raised: #2b2b2b;
--surface-muted: #262626;
--text-primary: #f5f5f5;
--text-secondary: #c7c7c7;
--text-disabled: #8f8f8f;
--accent: #60aef2;
--accent-hover: #79bcf5;
--accent-pressed: #4798de;
--accent-soft: #12324b;
--success: #6ccb8e;
--success-soft: #173d27;
--caution: #f3c45b;
--caution-soft: #493711;
--critical: #ff99a4;
--critical-soft: #4e1d22;
--info: #75b9f3;
--info-soft: #12324b;
--stroke: #5c5c5c;
--divider: #3c3c3c;
--focus: #ffffff;
--shadow: 0 8px 24px rgba(0, 0, 0, .32);
}
}
* { box-sizing: border-box; }
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
color: var(--text-primary);
background: var(--surface-base);
font: 14px/1.45 "Segoe UI Variable", "Segoe UI", "Microsoft YaHei UI", sans-serif;
}
button, select, input { font: inherit; }
a { color: inherit; }
button, select, a { -webkit-tap-highlight-color: transparent; }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
button:focus-visible, select:focus-visible, a:focus-visible { box-shadow: 0 0 0 4px var(--accent-soft); }
button, .button-link {
min-height: 32px;
border: 1px solid var(--stroke);
border-radius: var(--radius-control);
padding: 5px 14px;
color: var(--text-primary);
background: var(--surface-raised);
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
button:hover:not(:disabled), .button-link:hover { background: var(--surface-muted); }
button:active:not(:disabled), .button-link:active { transform: translateY(1px); }
button:disabled { color: var(--text-disabled); background: var(--surface-muted); cursor: not-allowed; }
.primary { color: white; border-color: transparent; background: var(--accent); }
.primary:hover:not(:disabled) { background: var(--accent-hover); }
.primary:active:not(:disabled) { background: var(--accent-pressed); }
.danger { color: var(--critical); border-color: var(--critical); background: transparent; }
.prototype-banner {
padding: 6px 16px;
color: #1b1b1b;
background: #f7cf46;
border-bottom: 1px solid #b08b00;
font-weight: 650;
text-align: center;
}
.app-bar {
min-height: 52px;
display: flex;
align-items: center;
gap: 16px;
padding: 8px 16px;
border-bottom: 1px solid var(--divider);
background: var(--surface-layer);
}
.brand { font-size: 16px; font-weight: 650; white-space: nowrap; }
.brand small { display: block; color: var(--text-secondary); font-size: 11px; font-weight: 400; }
.tabs { display: flex; align-self: stretch; align-items: stretch; gap: 2px; }
.tabs a {
min-height: 40px;
padding: 9px 16px 7px;
display: flex;
align-items: center;
border-bottom: 3px solid transparent;
color: var(--text-secondary);
text-decoration: none;
font-weight: 600;
}
.tabs a[aria-current="page"] { color: var(--text-primary); border-bottom-color: var(--accent); }
.app-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.app-actions label { color: var(--text-secondary); font-size: 12px; }
.app-actions select { min-height: 32px; border: 1px solid var(--stroke); border-radius: 4px; background: var(--surface-raised); color: var(--text-primary); padding: 4px 28px 4px 8px; }
main { max-width: 1480px; margin: 0 auto; padding: 16px; }
.prototype-tools {
display: grid;
2026-08-03 15:55:11 +08:00
grid-template-columns: minmax(240px, 1fr) minmax(180px, .55fr) auto;
gap: 12px;
align-items: end;
margin-bottom: 12px;
padding: 12px;
border: 1px dashed var(--stroke);
border-radius: var(--radius-card);
background: var(--surface-layer);
}
.prototype-tools label { display: grid; gap: 5px; color: var(--text-secondary); font-size: 12px; }
.prototype-tools select { min-height: 34px; width: 100%; border: 1px solid var(--stroke); border-radius: 4px; color: var(--text-primary); background: var(--surface-raised); padding: 5px 8px; }
.tool-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
.status-strip {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(260px, .9fr);
border: 1px solid var(--stroke);
border-radius: var(--radius-card);
background: var(--surface-raised);
overflow: hidden;
box-shadow: var(--shadow);
}
.health { display: grid; grid-template-columns: auto 1fr; gap: 4px 9px; padding: 12px 16px; border-right: 1px solid var(--divider); }
.health:nth-child(3) { border-right: 1px solid var(--divider); }
.health-mark { width: 10px; height: 10px; border-radius: 50%; background: currentColor; margin-top: 5px; }
.health strong { font-weight: 650; }
.health span:last-child { grid-column: 2; color: var(--text-secondary); font-size: 12px; }
.session-control { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; align-items: center; padding: 10px 12px; }
.session-copy { min-width: 0; }
.session-copy span { display: block; color: var(--text-secondary); font-size: 11px; }
.session-copy strong { display: block; overflow-wrap: anywhere; }
.session-control .primary { min-width: 112px; }
.disabled-reason { grid-column: 1 / -1; color: var(--text-secondary); font-size: 11px; }
.tone-success { color: var(--success); }
.tone-info { color: var(--info); }
.tone-caution { color: var(--caution); }
.tone-critical { color: var(--critical); }
.banner {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
margin-top: 12px;
padding: 12px 14px;
border: 1px solid currentColor;
border-radius: var(--radius-card);
background: var(--info-soft);
color: var(--info);
}
.banner[data-tone="success"] { background: var(--success-soft); color: var(--success); }
.banner[data-tone="caution"] { background: var(--caution-soft); color: var(--caution); }
.banner[data-tone="critical"] { background: var(--critical-soft); color: var(--critical); }
.banner-symbol { font-weight: 800; }
.banner strong { display: block; margin-bottom: 2px; }
.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; }
2026-08-03 15:55:11 +08:00
.left-pane { min-width: 0; min-height: 0; }
.stack { display: grid; grid-template-rows: auto minmax(240px, 1fr); gap: 12px; min-width: 0; }
2026-08-03 15:55:11 +08:00
.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; }
.card-header p { margin: 3px 0 0; color: var(--text-secondary); font-size: 12px; }
.badge { display: inline-flex; align-items: center; gap: 6px; min-height: 24px; padding: 2px 8px; border: 1px solid currentColor; border-radius: 999px; font-size: 12px; font-weight: 650; white-space: nowrap; }
.badge[data-tone="info"] { color: var(--info); background: var(--info-soft); }
.badge[data-tone="success"] { color: var(--success); background: var(--success-soft); }
.badge[data-tone="caution"] { color: var(--caution); background: var(--caution-soft); }
.badge[data-tone="critical"] { color: var(--critical); background: var(--critical-soft); }
.card-body { padding: 14px 16px 16px; }
.task-layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(180px, 1fr); gap: 16px; align-items: start; }
.task-copy { min-width: 0; }
.task-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 20px; margin: 0; }
.task-grid div { min-width: 0; }
.task-grid dt { color: var(--text-secondary); font-size: 12px; }
.task-grid dd { margin: 2px 0 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.task-grid .span-2 { grid-column: 1 / -1; }
.steps { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 7px; }
.steps li { display: grid; grid-template-columns: 20px 1fr auto; gap: 8px; align-items: start; color: var(--text-secondary); }
.step-mark { width: 18px; height: 18px; border: 1px solid var(--stroke); border-radius: 50%; display: grid; place-items: center; font-size: 11px; }
.steps li.done { color: var(--success); }
.steps li.active { color: var(--text-primary); font-weight: 650; }
.steps li.active .step-mark { border-color: var(--accent); box-shadow: inset 0 0 0 4px var(--accent-soft); }
.steps small { color: var(--text-secondary); font-weight: 400; }
.product-preview { margin: 0; min-width: 0; border: 1px solid var(--divider); border-radius: 6px; overflow: hidden; background: var(--surface-muted); }
.preview-stage { aspect-ratio: 4 / 3; min-height: 148px; display: grid; place-items: center; padding: 12px; border-bottom: 1px solid var(--divider); }
.preview-stage svg { width: 100%; height: 100%; max-height: 210px; }
.preview-empty { color: var(--text-secondary); text-align: center; }
.preview-empty strong { display: block; color: var(--text-primary); margin-bottom: 4px; }
.product-preview figcaption { padding: 9px 10px; color: var(--text-secondary); font-size: 11px; }
.product-preview figcaption span, .product-preview figcaption time { display: block; }
.log-card { display: flex; min-height: 240px; flex-direction: column; }
.log-tools { display: flex; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
.inline-metrics { display: flex; gap: 10px; flex-wrap: wrap; }
.inline-metrics span { color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
.inline-metrics strong { color: var(--text-primary); font-variant-numeric: tabular-nums; }
.log-list { list-style: none; padding: 0; margin: 0; min-height: 210px; max-height: 360px; overflow: auto; }
.log-list li { display: grid; grid-template-columns: 64px 78px 1fr; gap: 8px; padding: 8px 16px; border-bottom: 1px solid var(--divider); }
.log-list li:last-child { border-bottom: 0; }
.log-list time { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.log-level { font-size: 12px; font-weight: 650; }
.record-card { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.record-actions { display: flex; align-items: center; gap: 8px; }
.table-wrap { min-height: 420px; max-height: 720px; overflow: auto; flex: 1; }
.record-table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
.record-table th { position: sticky; top: 0; z-index: 1; padding: 9px 12px; color: var(--text-secondary); background: var(--surface-muted); border-bottom: 1px solid var(--stroke); text-align: left; font-size: 12px; }
.record-table th:last-child { width: 112px; }
.record-table td { padding: 10px 12px; border-bottom: 1px solid var(--divider); vertical-align: top; }
.record-table tbody tr { cursor: default; }
.record-table tbody tr:hover { background: var(--surface-muted); }
.record-table tbody tr[aria-selected="true"] { background: var(--accent-soft); }
.record-table tbody tr:focus-visible { position: relative; outline-offset: -3px; }
.record-title { display: block; font-weight: 650; overflow-wrap: anywhere; }
.record-meta { display: block; margin-top: 3px; color: var(--text-secondary); font-size: 11px; font-variant-numeric: tabular-nums; }
.record-status { display: inline-flex; padding: 2px 7px; border: 1px solid currentColor; border-radius: 999px; font-size: 11px; font-weight: 650; }
.record-status[data-tone="success"] { color: var(--success); }
.record-status[data-tone="info"] { color: var(--info); }
.record-status[data-tone="caution"] { color: var(--caution); }
.record-status[data-tone="critical"] { color: var(--critical); }
.table-empty { padding: 28px 16px; color: var(--text-secondary); text-align: center; }
.table-help { margin: 0; padding: 9px 12px; color: var(--text-secondary); border-top: 1px solid var(--divider); font-size: 11px; }
.context-menu { position: fixed; z-index: 20; min-width: 150px; padding: 4px; border: 1px solid var(--stroke); border-radius: 6px; background: var(--surface-raised); box-shadow: var(--shadow); }
.context-menu[hidden] { display: none; }
.context-menu button { width: 100%; justify-content: flex-start; border-color: transparent; }
dialog { width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 32px); overflow: auto; border: 1px solid var(--stroke); border-radius: 8px; padding: 0; color: var(--text-primary); background: var(--surface-raised); box-shadow: 0 24px 80px rgba(0,0,0,.3); }
dialog::backdrop { background: rgba(0,0,0,.42); }
.dialog-body { padding: 20px; }
.dialog-body h2 { margin: 0 0 8px; font-size: 18px; }
.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); }
2026-08-03 15:55:11 +08:00
.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; }
.detail-list dt { color: var(--text-secondary); }
.detail-list dd { margin: 0; overflow-wrap: anywhere; }
.timeline { margin: 14px 0 0; padding-left: 20px; color: var(--text-secondary); }
.timeline li + li { margin-top: 5px; }
.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.evidence-card { margin: 0; border: 1px solid var(--divider); border-radius: 6px; overflow: hidden; background: var(--surface-muted); }
.evidence-card:only-child { grid-column: 1 / -1; }
.evidence-card svg { display: block; width: 100%; aspect-ratio: 4 / 3; }
.evidence-card figcaption { padding: 8px 10px; color: var(--text-secondary); font-size: 11px; }
.record-result { padding: 14px 20px 18px; border-top: 1px solid var(--divider); }
.result-value { margin: 0; padding: 12px; border: 1px solid var(--divider); border-radius: 6px; background: var(--surface-muted); font-weight: 650; overflow-wrap: anywhere; }
.result-note { margin: 8px 0 0; color: var(--text-secondary); font-size: 12px; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
@media (max-width: 1007px) {
.workspace { grid-template-columns: 1fr; }
.stack { grid-template-rows: auto auto; }
.record-card { min-height: 480px; }
2026-08-03 15:55:11 +08:00
.workspace[data-mode="record"] .record-card { display: none; }
.log-list { max-height: 220px; }
}
@media (max-width: 640px) {
.app-bar { align-items: stretch; flex-wrap: wrap; gap: 4px 12px; }
.tabs { order: 3; width: 100%; }
.tabs a { flex: 1; justify-content: center; padding-inline: 8px; }
.app-actions label { position: absolute; clip: rect(0,0,0,0); }
main { padding: 8px; }
.prototype-tools { grid-template-columns: 1fr; }
.status-strip { grid-template-columns: 1fr; }
.health { border-right: 0; border-bottom: 1px solid var(--divider); }
.health:nth-child(3) { border-right: 0; }
.session-control { grid-template-columns: 1fr; }
.session-control .primary { width: 100%; min-height: 40px; }
.banner { grid-template-columns: auto minmax(0, 1fr); }
.banner button { grid-column: 1 / -1; width: 100%; }
.task-layout { grid-template-columns: 1fr; }
.task-grid { grid-template-columns: 1fr; }
.task-grid .span-2 { grid-column: auto; }
.preview-stage { aspect-ratio: 16 / 9; }
.card-header { align-items: stretch; flex-direction: column; }
.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; }
2026-08-03 15:55:11 +08:00
.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; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
}
@media (forced-colors: active) {
.prototype-banner, .banner, .badge, .primary, .health-mark { forced-color-adjust: auto; }
.card, .status-strip, .prototype-tools { box-shadow: none; }
.health-mark { border: 1px solid ButtonText; }
}
</style>
</head>
<body>
<div class="prototype-banner" role="note">PROTOTYPE - 仅供枚举交互,非实现依据;全部是假数据,不连接 ADB、web 服务或拼多多</div>
<header class="app-bar">
<div class="brand">采购工具<small>Windows / PySide6 前置原型</small></div>
<nav class="tabs" aria-label="主要页面">
<a href="desk-execution.html" aria-current="page">采购执行</a>
<a href="desk-device-settings.html">配置</a>
</nav>
<div class="app-actions">
<label for="theme">主题</label>
<select id="theme" aria-label="原型主题">
<option value="system">跟随系统</option>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
</header>
<main>
<section class="prototype-tools" aria-labelledby="prototype-tools-title">
<label id="prototype-tools-title" for="scenario">原型状态切换器(仅改变本页假数据)
<select id="scenario">
<option value="not_ready">未就绪 · web 不可达</option>
<option value="version_mismatch">未就绪 · 拼多多版本失配</option>
<option value="idle" selected>已就绪 · 尚未轮询</option>
<option value="polling">轮询中 · 暂无任务</option>
<option value="claimed">单趟执行 · 已领取</option>
<option value="selecting">单趟执行 · 选择规格</option>
<option value="gate1">单趟执行 · 闸门一</option>
<option value="quantity_gate2">单趟执行 · 数量与闸门二</option>
<option value="confirm_gate3">单趟执行 · 确认页与闸门三</option>
<option value="fencing">单趟执行 · 申请围栏中</option>
<option value="fence_failed">围栏失败或响应不明 · 不点击</option>
<option value="fenced">围栏后 · 核对同一提交</option>
<option value="waiting_payment">明确已创建 · 等人付款</option>
<option value="uncertain">点击结果不明 · 可能已创建</option>
<option value="security">安全校验 · 已停止</option>
<option value="payment_handoff">外部支付交接 · 已停止</option>
<option value="manual">围栏前异常 · 待人工</option>
<option value="failed_stop">连续失败 · 轮询已停止</option>
</select>
</label>
2026-08-03 15:55:11 +08:00
<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>
</div>
</section>
<section class="status-strip" aria-label="就绪状态">
<div class="health" id="web-health">
<span class="health-mark" aria-hidden="true"></span><strong>web 服务</strong><span>已连接 · 假环境</span>
</div>
<div class="health" id="device-health">
<span class="health-mark" aria-hidden="true"></span><strong>Android 设备</strong><span>FAKE-DEVICE-01 · 已解锁(模拟)</span>
</div>
<div class="health" id="version-health">
<span class="health-mark" aria-hidden="true"></span><strong>拼多多版本</strong><span>实际 / 已取证:均待 T-103 真机取证</span>
</div>
<div class="session-control" aria-label="轮询会话控制">
<div class="session-copy"><span>会话状态</span><strong id="session-state">未启动</strong></div>
<button type="button" class="primary" id="primary-action">开始轮询</button>
<div class="session-copy"><span>当前步骤</span><strong id="current-step">等待开始</strong></div>
<div class="disabled-reason" id="action-reason">web、设备和已取证 App 版本均就绪后才能开始。</div>
</div>
</section>
<section class="banner" id="state-banner" data-tone="success" aria-live="polite" aria-atomic="true">
<span class="banner-symbol" aria-hidden="true">i</span>
<div><strong id="banner-title">设备已就绪</strong><p id="banner-text">可以开始定时轮询。当前页面不会连接真实设备。</p></div>
<button type="button" id="context-action" hidden>查看说明</button>
</section>
2026-08-03 15:55:11 +08:00
<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>
<span class="badge" id="leg-badge" data-tone="success">空闲</span>
</div>
<div class="card-body">
<div class="task-layout">
<div class="task-copy">
<dl class="task-grid">
<div><dt>任务编号</dt><dd id="task-id">—</dd></div>
<div><dt>剩余租约</dt><dd id="lease">—</dd></div>
<div class="span-2"><dt>商品</dt><dd id="product">暂无商品;轮询会在安全就绪后领取一条任务</dd></div>
<div><dt>颜色分类</dt><dd id="color">—</dd></div>
<div><dt>尺码 / 数量</dt><dd id="size-qty">—</dd></div>
<div><dt>闸门一单价</dt><dd id="gate1-price">—</dd></div>
<div><dt>闸门二面板总额</dt><dd id="gate2-total">—</dd></div>
<div><dt>金额上限</dt><dd id="price-cap">—</dd></div>
<div class="span-2"><dt>提交围栏</dt><dd id="submission">未建立</dd></div>
</dl>
<ol class="steps" id="steps" aria-label="执行步骤"></ol>
</div>
<figure class="product-preview" aria-labelledby="preview-caption">
<div class="preview-stage">
<div class="preview-empty" id="preview-empty"><strong>暂无商品预览</strong>只有取得可信图片证据后才展示</div>
<svg id="product-preview-image" viewBox="0 0 320 240" role="img" aria-label="原型假商品预览,不是真实拼多多图片" hidden>
<rect width="320" height="240" fill="#e7f3ff"/>
<rect x="28" y="24" width="264" height="192" rx="12" fill="#ffffff" stroke="#9ac8ee" stroke-width="2"/>
<path d="M114 72l30-18h32l30 18 33 12-18 43-23-10v61h-76v-61l-23 10-18-43 33-12z" fill="#f5f5f5" stroke="#65788a" stroke-width="3"/>
<path d="M144 55c3 20 29 20 32 0" fill="none" stroke="#65788a" stroke-width="3"/>
<text x="160" y="208" text-anchor="middle" fill="#3a5268" font-size="13" font-family="Segoe UI, sans-serif">内联假证据 · 不访问拼多多</text>
</svg>
</div>
<figcaption id="preview-caption"><span id="preview-source">来源:尚无可信图片</span><time id="preview-time">采集时间:—</time></figcaption>
</figure>
</div>
</div>
</section>
<section class="card log-card" aria-labelledby="log-title">
<div class="card-header">
<div><h2 id="log-title">滚动日志</h2><p>最新事件在上;不含凭据、地址、手机号或页面全文</p></div>
<div class="log-tools">
<div class="inline-metrics" aria-label="会话指标"><span>下次轮询 <strong id="countdown">—</strong></span><span>连续失败 <strong id="failure-count">0 / 3</strong></span><span>本次完成 <strong id="completed-count">0</strong></span></div>
<button type="button" id="export-log">导出假诊断摘要</button>
</div>
</div>
<ul class="log-list" id="logs" aria-live="polite" aria-relevant="additions"></ul>
</section>
2026-08-03 15:55:11 +08:00
</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">
<div class="card-header">
<div><h2 id="record-title">执行记录</h2><p><span id="record-count">0</span> 条假记录 · 按采集时间倒序</p></div>
<div class="record-actions"><button type="button" id="view-record" disabled>查看所选记录</button></div>
</div>
<div class="table-wrap">
<table class="record-table" aria-describedby="record-help">
<thead><tr><th scope="col">标题</th><th scope="col">状态</th></tr></thead>
<tbody id="record-rows"></tbody>
</table>
<div class="table-empty" id="record-empty" hidden>暂无执行记录。开始轮询并产生结果后会显示在这里。</div>
</div>
2026-08-03 15:55:11 +08:00
<p class="table-help" id="record-help">单击选择;双击行、按 Enter 或使用“查看所选记录”在左侧打开详情。详情态可连续选择其他记录。</p>
</section>
</div>
</main>
<div class="context-menu" id="record-menu" role="menu" hidden>
<button type="button" id="record-menu-open" role="menuitem">查看记录详情</button>
</div>
<dialog id="info-dialog" aria-labelledby="dialog-title">
<div class="dialog-body">
<h2 id="dialog-title">安全说明</h2>
<p id="dialog-text"></p>
<p><strong>原型边界:</strong>本对话框不会调用 API、ADB 或任何平台动作。</p>
</div>
<div class="dialog-actions"><button type="button" class="primary" id="dialog-close">知道了</button></div>
</dialog>
<div id="announcer" class="sr-only" aria-live="assertive" aria-atomic="true"></div>
<script>
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const task = {
id: 'CB-20260803-0042',
product: '纯棉圆领短袖 T 恤(原型假数据)',
color: '白色', sizeQty: 'XL / 2 件', gate1Price: 'CNY 32.50', gate2Total: 'CNY 65.00', cap: 'CNY 80.00'
};
const records = [
{
id:'REC-DEMO-0042', taskId:'CB-20260803-0042', title:'纯棉圆领短袖 T 恤(原型假数据)', collectedAt:'2026-08-03T13:52:18+08:00',
status:'RECONCILIATION_REQUIRED', spec:'白色 | XL | 2 件', cap:'CNY 80.00', submission:'SUB-DEMO-0042 · 结果不明确',
note:'围栏后网络中断,订单可能已创建。只允许核查同一提交记录,不能重试。', evidence:['商品页原始截图(内联假证据)','规格面板原始截图(内联假证据)'],
timeline:['13:51:04 同一趟精确选择规格','13:51:41 三道价格闸门通过(假数据)','13:51:58 围栏已建立','13:52:18 点击结果不明确,转人工核查'],
result:'下单待核查:白色 | XL | CNY 32.50 | 2 件', resultNote:'订单可能已创建、支付未完成。系统不会再次提交,也不会付款。'
},
{
id:'REC-DEMO-0041', taskId:'CB-20260803-0041', title:'办公室桌面收纳盒(原型假数据)', collectedAt:'2026-08-03T12:24:09+08:00',
status:'ORDERING', spec:'透明 | L | 2 件', cap:'CNY 60.00', submission:'未建立',
note:'管理员已开始采购,当前单趟已完成规格与闸门一,正在设置并复核数量。', evidence:['商品页截图(内联假证据)','规格面板原始截图(内联假证据)'],
timeline:['12:22:31 领取一次性授权','12:23:04 精确匹配透明 / L','12:23:38 闸门一读取 CNY 24.00','12:24:09 正在复核数量与闸门二'],
result:'执行中:透明 | L | CNY 24.00 | 2 件', resultNote:'无需等待管理员中途确认;任一门禁失败即停止。'
},
{
id:'REC-DEMO-0038', taskId:'CB-20260803-0038', title:'标签打印纸补货(原型假数据)', collectedAt:'2026-08-03T10:06:42+08:00',
status:'WAITING_PAYMENT', spec:'白色 | 50×30 | 1 件', cap:'CNY 50.00', submission:'SUB-DEMO-0038 · SUBMITTED',
note:'已明确创建待付款订单,自动化已停止,等待人核对后自行付款。', evidence:['订单确认页内部原始截图(内联假证据)'],
timeline:['10:04:22 同一趟精确选择规格','10:05:10 Gate1 单价、Gate2 面板总额与 Gate3 确认金额通过','10:05:36 获得唯一围栏许可','10:06:42 明确创建待付款订单'],
result:'已创建待付款订单:白色 | 50×30 | CNY 46.00 | 1 件', resultNote:'系统只创建待付款订单,付款始终由人完成。'
},
{
id:'REC-DEMO-0032', taskId:'CB-20260803-0032', title:'仓库打包胶带(原型假数据)', collectedAt:'2026-08-03T09:18:05+08:00',
status:'NEEDS_MANUAL', spec:'透明 | 45mm | 4 件', cap:'CNY 55.00', submission:'未建立',
note:'目标数量的规格面板总额超过授权上限,已在围栏前转人工。', evidence:[],
timeline:['09:16:40 打开商品','09:17:22 匹配透明 / 45mm','09:18:05 闸门二面板总额超限,停止自动流程'],
result:'未采购:闸门一单价 CNY 12.00 | 闸门二面板总额 CNY 58.00 > 上限 CNY 55.00', resultNote:'没有建立围栏、创建订单或付款。'
}
].sort((a, b) => b.collectedAt.localeCompare(a.collectedAt));
const recordStatus = {
RECONCILIATION_REQUIRED:{label:'结果待核查',tone:'critical'},
ORDERING:{label:'采购执行中',tone:'info'},
WAITING_PAYMENT:{label:'待付款',tone:'success'},
NEEDS_MANUAL:{label:'转人工',tone:'caution'}
};
let currentRecordId = records[0]?.id || null;
let recordFocusReturn = null;
2026-08-03 15:55:11 +08:00
let workspaceMode = 'live';
let detailRequestId = 0;
const baseSteps = {
idle: [],
single: ['核对一次性授权并打开商品', '精确选择颜色分类与尺码', '闸门一:读取 SKU 单价并校验上限', '设置数量、读回并读取闸门二面板总额', '确认页执行闸门三同额与控件唯一校验', '申请服务端提交围栏', '首次许可后持久化并内部点击一次', '调和同一提交结果']
};
const scenarios = {
not_ready: { tone:'critical', title:'web 服务不可达', text:'未开始轮询。检查本地网络和服务地址;已完成的本地证据会保留待补传。', web:'bad', device:'ok', version:'ok', leg:'未就绪', legTone:'critical', action:'开始轮询', enabled:false, reason:'web 服务未就绪,不能领取任务。', session:'未启动', step:'连接检查', kind:'idle', active:-1, countdown:'—', fail:'1 / 3' },
version_mismatch: { tone:'critical', title:'拼多多版本与证据不一致', text:'已 fail closed:停止领取真机任务。下一步是由人重新取证并记录 App 版本,不能沿用旧判据。', web:'ok', device:'ok', version:'bad', leg:'版本失配', legTone:'critical', action:'开始轮询', enabled:false, reason:'App 版本失配;必须先完成本项目真机取证。', session:'已停止', step:'等待重新取证', kind:'idle', active:-1, countdown:'—', fail:'0 / 3' },
idle: { tone:'success', title:'设备已就绪(模拟)', text:'三项就绪条件在本场景中均为模拟通过,可以演示定时轮询;不会连接真实设备。', web:'ok', device:'ok', version:'ok', leg:'空闲', legTone:'success', action:'开始轮询', enabled:true, reason:'点击只切换原型状态,不发起网络请求。', session:'未启动', step:'等待开始', kind:'idle', active:-1, countdown:'—', fail:'0 / 3' },
polling: { tone:'info', title:'正在轮询,暂无待领任务', text:'无任务是正常空状态,不计入失败;将按配置间隔继续检查。', web:'ok', device:'ok', version:'ok', leg:'轮询中', legTone:'info', action:'停止轮询', enabled:true, reason:'停止后不再领取新任务。', session:'运行中', step:'等待下一轮', kind:'idle', active:-1, countdown:'00:12', fail:'0 / 3', count:2 },
claimed: { tone:'info', title:'已领取管理员授权的采购任务', text:'任务字段与最高总价已锁定;当前只准备打开目标商品,尚未建立提交围栏。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 已领取', legTone:'info', action:'真机步骤执行中', enabled:false, reason:'当前任务已开始,停止轮询只应阻止下一次领取,不能硬取消本任务。', session:'运行中', step:'核对授权与商品', kind:'single', active:0, countdown:'暂停', fail:'0 / 3', lease:'04:48' },
selecting: { tone:'info', title:'正在精确选择颜色与尺码', text:'只在已取证维度容器内精确唯一匹配并读回;缺失、重复或版本变化立即停止。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 选择规格', legTone:'info', action:'真机步骤执行中', enabled:false, reason:'当前受控步骤不能被硬取消。', session:'运行中', step:'精确选择规格', kind:'single', active:1, countdown:'暂停', fail:'0 / 3', lease:'04:18' },
gate1: { tone:'info', title:'闸门一:读取 SKU 单价', text:'当前规格单价必须唯一可读,单价乘数量不得超过管理员授权的最高总价。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 闸门一', legTone:'info', action:'安全校验进行中', enabled:false, reason:'价格不可读或超上限时立即停止。', session:'运行中', step:'闸门一:读取 SKU 单价', kind:'single', active:2, countdown:'暂停', fail:'0 / 3', lease:'03:58' },
quantity_gate2: { tone:'info', title:'数量复核与闸门二', text:'数量必须读回为 2;目标规格不能漂移,面板总额 CNY 65.00 必须唯一可读且不超过 CNY 80.00。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 闸门二', legTone:'info', action:'安全校验进行中', enabled:false, reason:'数量、规格、总额歧义或超限都停止。', session:'运行中', step:'数量读回与闸门二', kind:'single', active:3, countdown:'暂停', fail:'0 / 3', lease:'03:31' },
confirm_gate3: { tone:'caution', title:'确认页与闸门三', text:'正在核对规格、数量、应付总额严格等于闸门二面板总额,并确认提交控件唯一;此时没有点击许可。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 闸门三', legTone:'caution', action:'安全校验进行中', enabled:false, reason:'没有服务端围栏许可,绝不能点击提交订单。', session:'运行中', step:'闸门三:核对应付总额', kind:'single', active:4, countdown:'暂停', fail:'0 / 3', lease:'02:58' },
fencing: { tone:'caution', title:'正在申请服务端提交围栏', text:'等待首次明确的 click_permitted。超时、冲突、网络失败或响应不明都不会点击。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 申请围栏', legTone:'caution', action:'等待围栏响应', enabled:false, reason:'申请中禁止取消、关闭或换 key 重复申请。', session:'运行中', step:'申请唯一提交围栏', kind:'single', active:5, countdown:'暂停', fail:'0 / 3', lease:'02:31' },
fence_failed: { tone:'critical', title:'未获得首次明确点击许可', text:'围栏失败或响应不明:没有点击提交订单。只核查同一幂等键,不能直接重试。', web:'bad', device:'ok', version:'ok', leg:'单趟 · 未获许可', legTone:'critical', action:'打开围栏核查说明', enabled:true, safeAction:'review', reason:'只核查围栏状态,不触发提交。', session:'已停止', step:'核查幂等键', kind:'single', active:5, countdown:'—', fail:'1 / 3', submission:'围栏状态未知 · 幂等键 DEMO-FENCE-042' },
fenced: { tone:'caution', title:'围栏已建立,只核对同一提交', text:'submission_id 已固定。内部流程至多点击一次;界面不提供提交、重领、放弃或重试。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 已围栏', legTone:'caution', action:'打开同一提交核查说明', enabled:true, safeAction:'review', reason:'只能查看与调和同一提交记录。', session:'受控执行', step:'调和订单结果', kind:'single', active:7, countdown:'暂停', fail:'0 / 3', lease:'01:49', submission:'SUB-DEMO-0042 · FENCED' },
waiting_payment: { tone:'success', title:'已明确创建待付款订单', text:'自动化已停止。系统不会付款;请人在拼多多核对后自行付款,再到采购服务标记。', web:'ok', device:'ok', version:'ok', leg:'WAITING_PAYMENT', legTone:'success', action:'查看待付款说明', enabled:true, safeAction:'paymentInfo', reason:'仅显示人工收口说明,不执行支付。', session:'已停止', step:'等待人工付款', kind:'single', active:8, countdown:'—', fail:'0 / 3', count:5, submission:'SUB-DEMO-0042 · SUBMITTED' },
uncertain: { tone:'critical', title:'订单可能已创建,结果不明确', text:'保持金额预留和永久围栏,只核查 SUB-DEMO-0042;禁止重新领取、释放授权或再次点击。', web:'ok', device:'ok', version:'ok', leg:'RECONCILIATION_REQUIRED', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'review', reason:'核查不会发起新的下单。可另行导出假证据摘要。', session:'已停止', step:'人工核查同一提交', kind:'single', active:7, countdown:'—', fail:'0 / 3', submission:'SUB-DEMO-0042 · 结果不明确' },
security: { tone:'critical', title:'检测到安全校验,已停止', text:'不绕过验证码、风控、人脸或短信。若已围栏,只调和同一提交;围栏前转人工。', web:'ok', device:'ok', version:'ok', leg:'待人工 · 安全校验', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'review', reason:'只显示核查说明,不提供绕过或继续。', session:'已停止', step:'等待人工处理', kind:'single', active:7, countdown:'—', fail:'1 / 3', submission:'SUB-DEMO-0042 · RECONCILIATION_REQUIRED' },
payment_handoff: { tone:'critical', title:'进入外部支付交接,已停止', text:'订单可能已创建、支付未完成。不会读取或输入支付凭据,也没有“继续付款”按钮。', web:'ok', device:'ok', version:'ok', leg:'待人工 · 外部支付', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'paymentInfo', reason:'只显示人工核查与付款边界。', session:'已停止', step:'人工核查同一提交', kind:'single', active:7, countdown:'—', fail:'0 / 3', submission:'SUB-DEMO-0042 · HANDED_OFF' },
manual: { tone:'caution', title:'任务已转人工:闸门二总额超限', text:'目标数量面板总额 CNY 85.00 超过最高总价 CNY 80.00。围栏未建立,没有创建订单;需管理员核查后重置。', web:'ok', device:'ok', version:'ok', leg:'NEEDS_MANUAL', legTone:'caution', action:'导出假诊断摘要', enabled:true, safeAction:'export', reason:'导出只生成本地假文本,不含完整 XML、页面全文或敏感字段。', session:'继续轮询其他任务', step:'当前任务已移交', kind:'single', active:3, countdown:'00:11', fail:'0 / 3', submission:'未建立' },
failed_stop: { tone:'critical', title:'连续失败达到阈值,轮询已停止', text:'最近三次均为服务连接失败。检查设置和网络;修复后重新开始普通轮询,不会重试任何提交点击。', web:'bad', device:'ok', version:'ok', leg:'自动停止', legTone:'critical', action:'前往配置', enabled:true, safeAction:'settings', reason:'先修复连接;不会自动恢复轮询。', session:'已停止', step:'等待修复', kind:'idle', active:-1, countdown:'—', fail:'3 / 3', count:4 }
};
const sessionControls = {
not_ready:{label:'开始轮询',enabled:false,reason:'web 服务未就绪,不能领取任务。'},
version_mismatch:{label:'开始轮询',enabled:false,reason:'App 版本失配,必须先完成本项目真机取证。'},
idle:{label:'开始轮询',enabled:true,reason:'只启动领取会话;只有已获管理员开始采购授权的任务可执行。'},
polling:{label:'停止轮询',enabled:true,reason:'停止后不再领取新任务。'},
claimed:{label:'停止轮询',enabled:false,reason:'当前任务已领取;停止只应阻止下次领取,不能硬取消本任务。'},
selecting:{label:'停止轮询',enabled:false,reason:'受控真机步骤执行中,禁止硬停止。'},
gate1:{label:'停止轮询',enabled:false,reason:'价格闸门执行中,禁止硬停止。'},
quantity_gate2:{label:'停止轮询',enabled:false,reason:'数量与闸门二执行中,禁止硬停止。'},
confirm_gate3:{label:'停止轮询',enabled:false,reason:'确认页只允许安全校验,禁止硬停止或额外点击。'},
fencing:{label:'停止轮询',enabled:false,reason:'围栏申请中,禁止关闭、取消或重复申请。'},
fence_failed:{label:'开始轮询',enabled:false,reason:'先核查围栏状态;不能用重新轮询绕过不明确结果。'},
fenced:{label:'停止轮询',enabled:false,reason:'已建立围栏,只能调和同一提交。'},
waiting_payment:{label:'开始轮询',enabled:false,reason:'先由人核对待付款订单并完成收口。'},
uncertain:{label:'开始轮询',enabled:false,reason:'订单结果不明确,只能核查同一提交。'},
security:{label:'开始轮询',enabled:false,reason:'安全校验已转人工,不允许自动继续。'},
payment_handoff:{label:'开始轮询',enabled:false,reason:'外部支付交接中,自动化保持停止。'},
manual:{label:'停止轮询',enabled:true,reason:'当前任务已移交,仍可安全停止普通轮询。'},
failed_stop:{label:'开始轮询',enabled:false,reason:'先到配置页修复连接;不会自动恢复轮询。'}
};
function setHealth(id, state, detail) {
const el = $(id);
el.className = `health tone-${state === 'ok' ? 'success' : state === 'bad' ? 'critical' : 'caution'}`;
if (detail) el.querySelector('span:last-child').textContent = detail;
}
function renderSteps(kind, active) {
const items = baseSteps[kind] || [];
$('steps').replaceChildren(...items.map((label, index) => {
const li = document.createElement('li');
if (active >= items.length || index < active) li.className = 'done';
else if (index === active) li.className = 'active';
const mark = document.createElement('span'); mark.className = 'step-mark'; mark.textContent = index < active || active >= items.length ? '✓' : String(index + 1);
const text = document.createElement('span'); text.textContent = label;
const state = document.createElement('small'); state.textContent = index < active || active >= items.length ? '已完成' : index === active ? '当前' : '等待';
li.append(mark, text, state); return li;
}));
}
function appendLog(level, message) {
const li = document.createElement('li');
const now = new Date();
const time = document.createElement('time'); time.textContent = now.toLocaleTimeString('zh-CN', {hour12:false});
const badge = document.createElement('span'); badge.className = `log-level tone-${level === '停止' ? 'critical' : level === '注意' ? 'caution' : level === '完成' ? 'success' : 'info'}`; badge.textContent = level;
const text = document.createElement('span'); text.textContent = message;
li.append(time, badge, text); $('logs').prepend(li);
while ($('logs').children.length > 8) $('logs').lastElementChild.remove();
}
function formatRecordTime(value) {
return value.slice(5, 16).replace('T', ' ');
}
function selectRecord(id, focus = false) {
currentRecordId = id;
2026-08-03 15:55:11 +08:00
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;
2026-08-03 15:55:11 +08:00
if (selected) selectedRow = row;
if (selected && focus) row.focus();
}
$('view-record').disabled = !records.some((record) => record.id === id);
2026-08-03 15:55:11 +08:00
if (workspaceMode === 'record') {
recordFocusReturn = selectedRow || recordFocusReturn;
renderRecordDetail(id);
}
}
function renderRecords() {
const rows = records.map((record) => {
const status = recordStatus[record.status] || {label:record.status,tone:'info'};
const row = document.createElement('tr');
row.dataset.recordId = record.id;
row.setAttribute('aria-selected', 'false');
const titleCell = document.createElement('td');
const title = document.createElement('span'); title.className = 'record-title'; title.textContent = record.title;
const meta = document.createElement('span'); meta.className = 'record-meta'; meta.textContent = `${record.taskId} · ${formatRecordTime(record.collectedAt)}`;
titleCell.append(title, meta);
const statusCell = document.createElement('td');
const badge = document.createElement('span'); badge.className = 'record-status'; badge.dataset.tone = status.tone; badge.textContent = status.label;
statusCell.append(badge); row.append(titleCell, statusCell);
row.addEventListener('click', () => selectRecord(record.id));
row.addEventListener('dblclick', (event) => { if (event.target.closest('button,a,input,select')) return; selectRecord(record.id); openRecord(record.id, row); });
row.addEventListener('keydown', (event) => {
if (event.key === 'Enter') { event.preventDefault(); openRecord(record.id, row); return; }
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
const rows = [...$('record-rows').rows]; const index = rows.indexOf(row); const delta = event.key === 'ArrowDown' ? 1 : -1;
const next = rows[Math.max(0, Math.min(rows.length - 1, index + delta))]; if (next) selectRecord(next.dataset.recordId, true);
}
if (event.key === 'F10' && event.shiftKey) { event.preventDefault(); showRecordMenu(record.id, row.getBoundingClientRect().left + 16, row.getBoundingClientRect().top + 24); }
});
row.addEventListener('contextmenu', (event) => { event.preventDefault(); selectRecord(record.id); showRecordMenu(record.id, event.clientX, event.clientY); });
return row;
});
$('record-rows').replaceChildren(...rows);
$('record-count').textContent = String(records.length);
$('record-empty').hidden = records.length > 0;
if (records.length) selectRecord(records.some((record) => record.id === currentRecordId) ? currentRecordId : records[0].id);
}
function showRecordMenu(id, x, y) {
currentRecordId = id;
const menu = $('record-menu'); menu.hidden = false;
menu.style.left = `${Math.min(x, innerWidth - 170)}px`; menu.style.top = `${Math.min(y, innerHeight - 54)}px`;
$('record-menu-open').focus();
}
function hideRecordMenu() { $('record-menu').hidden = true; }
function createEvidence(label) {
const figure = document.createElement('figure'); figure.className = 'evidence-card';
const svg = $('product-preview-image').cloneNode(true); svg.hidden = false; svg.removeAttribute('id'); svg.setAttribute('aria-label', `${label},原型内联假证据`);
const caption = document.createElement('figcaption'); caption.textContent = `${label} · 不访问真实拼多多`;
figure.append(svg, caption); return figure;
}
2026-08-03 15:55:11 +08:00
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};
2026-08-03 15:55:11 +08:00
$('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;
2026-08-03 15:55:11 +08:00
$('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];
const sessionControl = sessionControls[key];
$('state-banner').dataset.tone = s.tone;
$('banner-title').textContent = s.title; $('banner-text').textContent = s.text;
setHealth('web-health', s.web, s.web === 'bad' ? '不可达(模拟)· 已停止领取' : '已连接 · 假环境');
setHealth('device-health', s.device, 'FAKE-DEVICE-01 · 已解锁(模拟)');
setHealth('version-health', s.version, s.version === 'bad' ? '模拟失配 · 必须重新取证' : '模拟匹配 · 真实版本值待 T-103 取证');
$('leg-badge').textContent = s.leg; $('leg-badge').dataset.tone = s.legTone;
const hasTask = !['not_ready','version_mismatch','idle','polling','failed_stop'].includes(key);
$('task-subtitle').textContent = hasTask ? '本页所有任务字段均为假数据' : '尚未领取任务';
$('task-id').textContent = hasTask ? task.id : '—'; $('lease').textContent = s.lease || '—';
$('product').textContent = hasTask ? task.product : '暂无商品;轮询会在安全就绪后领取一条任务';
$('color').textContent = hasTask ? task.color : '—'; $('size-qty').textContent = hasTask ? task.sizeQty : '—';
$('gate1-price').textContent = hasTask && ['gate1','quantity_gate2','confirm_gate3','fencing','fence_failed','fenced','waiting_payment','uncertain','security','payment_handoff','manual'].includes($('scenario').value) ? task.gate1Price : '—';
$('gate2-total').textContent = hasTask && ['quantity_gate2','confirm_gate3','fencing','fence_failed','fenced','waiting_payment','uncertain','security','payment_handoff'].includes($('scenario').value) ? task.gate2Total : '—';
$('price-cap').textContent = hasTask ? task.cap : '—';
$('submission').textContent = s.submission || '未建立';
$('preview-empty').hidden = hasTask; $('product-preview-image').hidden = !hasTask;
$('preview-source').textContent = hasTask ? '来源:内部原始商品截图(内联假证据)' : '来源:尚无可信图片';
$('preview-time').textContent = hasTask ? '采集时间:2026-08-03 13:51(假数据)' : '采集时间:—';
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;
2026-08-03 15:55:11 +08:00
$('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 : '';
$('context-action').dataset.safeAction = s.safeAction || '';
if (announce) { $('announcer').textContent = `${s.title}。${s.text}`; appendLog(s.tone === 'critical' ? '停止' : s.tone === 'caution' ? '注意' : s.tone === 'success' ? '完成' : '状态', `切换原型状态:${s.title}`); }
}
function showDialog(title, text) {
$('dialog-title').textContent = title; $('dialog-text').textContent = text; $('info-dialog').showModal();
}
function exportDemo() {
const blob = new Blob(['采购工具原型假诊断摘要\n无真实任务、无凭据、无页面全文。\n'], {type:'text/plain;charset=utf-8'});
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'purchase-tool-prototype-diagnostic.txt'; a.click(); URL.revokeObjectURL(url);
appendLog('完成', '已生成本地假诊断摘要;没有外部网络请求。');
}
$('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}`; });
2026-08-03 15:55:11 +08:00
$('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);
});
$('context-action').addEventListener('click', () => {
const action = $('context-action').dataset.safeAction;
if (action === 'settings') { location.href = 'desk-device-settings.html'; return; }
if (action === 'export') { exportDemo(); return; }
if (action === 'paymentInfo') { showDialog('人工付款边界', '自动化已经停止。本系统不会点击支付、免密支付、先用后付或任何扣款控件;订单状态需要人核查。'); return; }
showDialog('只核查同一提交记录', '围栏后只能围绕 SUB-DEMO-0042 调和。这里没有重新领取、放弃授权、重新申请围栏或再次提交的动作。');
});
$('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)));
2026-08-03 15:55:11 +08:00
$('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(); });
2026-08-03 15:55:11 +08:00
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', () => {
const unsafe = ['claimed','selecting','gate1','quantity_gate2','confirm_gate3','fencing','fenced'].includes($('scenario').value);
showDialog(unsafe ? '当前不能直接关闭' : '关闭窗口协议', unsafe ? '真机或围栏流程处于受控阶段。原生实现需先完成安全停靠或保持同一 submission 调和,不能让关闭看起来等于“可以重来”。' : '原生实现关闭窗口后立即停止普通轮询,不留后台进程。此 HTML 只演示规则,不会关闭浏览器。');
});
$('dialog-close').addEventListener('click', () => $('info-dialog').close());
$('info-dialog').addEventListener('click', (e) => { if (e.target === $('info-dialog')) $('info-dialog').close(); });
renderRecords(); render('idle', false); appendLog('状态', '原型已加载;未连接任何服务或设备。');
})();
</script>
</body>
</html>