358 lines
30 KiB
HTML
358 lines
30 KiB
HTML
<!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>cmbuyer 桌面端原型 · 设备与参数</title>
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
color-scheme: light dark;
|
|||
|
|
--surface-base: #f3f3f3;
|
|||
|
|
--surface-layer: rgba(255,255,255,.86);
|
|||
|
|
--surface-raised: #fff;
|
|||
|
|
--surface-muted: #f8f8f8;
|
|||
|
|
--text-primary: #1b1b1b;
|
|||
|
|
--text-secondary: #5d5d5d;
|
|||
|
|
--text-disabled: #8a8a8a;
|
|||
|
|
--accent: #005fb8;
|
|||
|
|
--accent-hover: #00549f;
|
|||
|
|
--accent-soft: #e7f3ff;
|
|||
|
|
--success: #0f6b38;
|
|||
|
|
--success-soft: #e7f5ec;
|
|||
|
|
--caution: #8a5600;
|
|||
|
|
--caution-soft: #fff4ce;
|
|||
|
|
--critical: #b10e1e;
|
|||
|
|
--critical-soft: #fde7e9;
|
|||
|
|
--stroke: #d0d0d0;
|
|||
|
|
--divider: #e5e5e5;
|
|||
|
|
--focus: #000;
|
|||
|
|
--shadow: 0 8px 24px rgba(0,0,0,.08);
|
|||
|
|
--radius-control: 4px;
|
|||
|
|
--radius-card: 8px;
|
|||
|
|
}
|
|||
|
|
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-soft:#12324b;--success:#6ccb8e;--success-soft:#173d27;--caution:#f3c45b;--caution-soft:#493711;
|
|||
|
|
--critical:#ff99a4;--critical-soft:#4e1d22;--stroke:#5c5c5c;--divider:#3c3c3c;--focus:#fff;--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-soft:#12324b;--success:#6ccb8e;--success-soft:#173d27;--caution:#f3c45b;--caution-soft:#493711;
|
|||
|
|
--critical:#ff99a4;--critical-soft:#4e1d22;--stroke:#5c5c5c;--divider:#3c3c3c;--focus:#fff;--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,input,select { font:inherit; }
|
|||
|
|
:focus-visible { outline:2px solid var(--focus); outline-offset:2px; }
|
|||
|
|
button:focus-visible,input: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:disabled,input:disabled,select:disabled { color:var(--text-disabled); background:var(--surface-muted); cursor:not-allowed; }
|
|||
|
|
.primary { color:#fff; border-color:transparent; background:var(--accent); }
|
|||
|
|
.primary:hover:not(:disabled) { background:var(--accent-hover); }
|
|||
|
|
.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,.prototype-tools select { min-height:32px; border:1px solid var(--stroke); border-radius:4px; color:var(--text-primary); background:var(--surface-raised); padding:4px 28px 4px 8px; }
|
|||
|
|
main { max-width:1120px; margin:0 auto; padding:16px; }
|
|||
|
|
.prototype-tools { display:grid; grid-template-columns:minmax(240px,1fr) auto; align-items:end; gap:12px; 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 { width:100%; }
|
|||
|
|
.tool-buttons { display:flex; flex-wrap:wrap; gap:8px; }
|
|||
|
|
.banner { display:grid; grid-template-columns:auto 1fr; gap:10px; margin-bottom:12px; padding:12px 14px; border:1px solid currentColor; border-radius:var(--radius-card); color:var(--accent); background:var(--accent-soft); }
|
|||
|
|
.banner[data-tone="success"] { color:var(--success); background:var(--success-soft); }
|
|||
|
|
.banner[data-tone="caution"] { color:var(--caution); background:var(--caution-soft); }
|
|||
|
|
.banner[data-tone="critical"] { color:var(--critical); background:var(--critical-soft); }
|
|||
|
|
.banner strong { display:block; margin-bottom:2px; }
|
|||
|
|
.banner p { margin:0; color:var(--text-primary); }
|
|||
|
|
.banner-symbol { font-weight:800; }
|
|||
|
|
.layout { display:grid; grid-template-columns:minmax(0,1fr) 280px; gap:12px; align-items:start; }
|
|||
|
|
.form-stack,.side-stack { display:grid; gap:12px; }
|
|||
|
|
.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 { padding:14px 16px 10px; border-bottom:1px solid var(--divider); }
|
|||
|
|
.card-header h1,.card-header h2 { margin:0; font-size:17px; }
|
|||
|
|
.card-header p { margin:3px 0 0; color:var(--text-secondary); font-size:12px; }
|
|||
|
|
.card-body { padding:14px 16px 16px; }
|
|||
|
|
.fields { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px 20px; }
|
|||
|
|
.field { min-width:0; display:grid; align-content:start; gap:5px; }
|
|||
|
|
.field.full { grid-column:1/-1; }
|
|||
|
|
.field label,.group-label { font-weight:600; }
|
|||
|
|
.required { color:var(--critical); }
|
|||
|
|
.field input,.field select { width:100%; min-height:34px; padding:5px 9px; color:var(--text-primary); background:var(--surface-raised); border:1px solid var(--stroke); border-radius:var(--radius-control); }
|
|||
|
|
.field input[readonly] { background:var(--surface-muted); color:var(--text-secondary); }
|
|||
|
|
.field input[aria-invalid="true"],.field select[aria-invalid="true"] { border-color:var(--critical); box-shadow:0 0 0 1px var(--critical); }
|
|||
|
|
.help { color:var(--text-secondary); font-size:12px; }
|
|||
|
|
.error { min-height:18px; color:var(--critical); font-size:12px; }
|
|||
|
|
.radio-row { display:flex; flex-wrap:wrap; gap:8px 20px; min-height:34px; align-items:center; }
|
|||
|
|
.radio-row label { display:flex; align-items:center; gap:7px; font-weight:400; }
|
|||
|
|
input[type="radio"] { width:16px; min-height:16px; accent-color:var(--accent); }
|
|||
|
|
.summary-list { margin:0; display:grid; gap:12px; }
|
|||
|
|
.summary-list div { display:grid; gap:2px; }
|
|||
|
|
.summary-list dt { color:var(--text-secondary); font-size:12px; }
|
|||
|
|
.summary-list dd { margin:0; overflow-wrap:anywhere; }
|
|||
|
|
.status { display:inline-flex; align-items:center; gap:7px; font-weight:650; }
|
|||
|
|
.status::before { content:""; width:9px; height:9px; border-radius:50%; background:currentColor; }
|
|||
|
|
.status.ok { color:var(--success); }
|
|||
|
|
.status.warn { color:var(--caution); }
|
|||
|
|
.status.bad { color:var(--critical); }
|
|||
|
|
.check-list { margin:0; padding-left:20px; display:grid; gap:8px; }
|
|||
|
|
.check-list li::marker { color:var(--success); }
|
|||
|
|
.form-actions { margin-top:12px; display:grid; grid-template-columns:1fr auto; gap:12px; align-items:center; padding:12px 16px; border:1px solid var(--stroke); border-radius:var(--radius-card); background:var(--surface-raised); box-shadow:var(--shadow); }
|
|||
|
|
.action-text { color:var(--text-secondary); font-size:12px; }
|
|||
|
|
.action-buttons { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:8px; }
|
|||
|
|
dialog { width:min(560px,calc(100vw - 32px)); 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; }
|
|||
|
|
.dialog-actions { display:flex; justify-content:flex-end; gap:8px; padding:12px 20px; border-top:1px solid var(--divider); }
|
|||
|
|
.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:800px) { .layout { grid-template-columns:1fr; } .side-stack { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
|||
|
|
@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; }
|
|||
|
|
.fields,.side-stack { grid-template-columns:1fr; } .field.full { grid-column:auto; }
|
|||
|
|
.form-actions { grid-template-columns:1fr; }
|
|||
|
|
.action-buttons { justify-content:stretch; } .action-buttons button { flex:1; min-height:40px; }
|
|||
|
|
}
|
|||
|
|
@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,.primary,.status::before { forced-color-adjust:auto; } .card,.form-actions,.prototype-tools { box-shadow:none; } .status::before { border:1px solid ButtonText; } }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="prototype-banner" role="note">PROTOTYPE - 仅供枚举交互,非实现依据;全部是假数据,不执行连接检查或保存真实配置</div>
|
|||
|
|
<header class="app-bar">
|
|||
|
|
<div class="brand">cmbuyer 执行器<small>Windows / PySide6 前置原型</small></div>
|
|||
|
|
<nav class="tabs" aria-label="主要页面">
|
|||
|
|
<a href="desk-execution.html" id="execution-link">采购执行</a>
|
|||
|
|
<a href="desk-device-settings.html" aria-current="page">设备与参数</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="default">默认 · 可编辑</option>
|
|||
|
|
<option value="invalid">校验失败 · 保留输入</option>
|
|||
|
|
<option value="checking">连接检查中</option>
|
|||
|
|
<option value="connected">连接检查通过</option>
|
|||
|
|
<option value="web_error">web 服务不可达</option>
|
|||
|
|
<option value="mismatch">App 版本失配</option>
|
|||
|
|
<option value="saved">保存成功</option>
|
|||
|
|
<option value="running">轮询运行中 · 配置冻结</option>
|
|||
|
|
</select>
|
|||
|
|
</label>
|
|||
|
|
<div class="tool-buttons"><button type="button" id="reset">复位假数据</button></div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="banner" id="status-banner" data-tone="caution" 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">先确认显式 serial、ADB 和 web 地址,再运行不会打开 App 的模拟检查。</p></div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<form id="settings-form" novalidate>
|
|||
|
|
<div class="layout">
|
|||
|
|
<div class="form-stack">
|
|||
|
|
<section class="card" aria-labelledby="device-title">
|
|||
|
|
<div class="card-header"><h1 id="device-title">设备档案</h1><p>必须显式指定 serial;不自动选择设备</p></div>
|
|||
|
|
<div class="card-body fields">
|
|||
|
|
<div class="field">
|
|||
|
|
<label for="profile">档案名称 <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="profile" name="profile" value="采购手机(假数据)" required aria-describedby="profile-help profile-error">
|
|||
|
|
<span class="help" id="profile-help">用于本机识别,不上传真实个人信息。</span><span class="error" id="profile-error"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label for="serial">ADB serial <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="serial" name="serial" value="FAKE-DEVICE-01" required spellcheck="false" aria-describedby="serial-help serial-error">
|
|||
|
|
<span class="help" id="serial-help">USB / WiFi 同时在线时仍只认这里的精确值。</span><span class="error" id="serial-error"></span>
|
|||
|
|
</div>
|
|||
|
|
<fieldset class="field full" style="border:0;padding:0;margin:0">
|
|||
|
|
<legend class="group-label">连接方式</legend>
|
|||
|
|
<div class="radio-row"><label><input type="radio" name="transport" value="usb" checked>USB ADB</label><label><input type="radio" name="transport" value="wifi">WiFi ADB</label></div>
|
|||
|
|
<span class="help">这是展示标签,不改变 serial 的唯一身份规则。</span>
|
|||
|
|
</fieldset>
|
|||
|
|
<div class="field full">
|
|||
|
|
<label for="adb-path">ADB 可执行文件 <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="adb-path" name="adb_path" value="C:\\Android\\platform-tools\\adb.exe" required spellcheck="false" aria-describedby="adb-help adb-error">
|
|||
|
|
<span class="help" id="adb-help">原生实现使用文件选择器;此 HTML 不访问本地文件系统。</span><span class="error" id="adb-error"></span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="card" aria-labelledby="service-title">
|
|||
|
|
<div class="card-header"><h2 id="service-title">web 服务与设备身份</h2><p>设备凭据与管理员会话隔离</p></div>
|
|||
|
|
<div class="card-body fields">
|
|||
|
|
<div class="field full">
|
|||
|
|
<label for="web-url">web 服务地址 <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="web-url" name="web_url" type="url" value="http://127.0.0.1:8080" required spellcheck="false" aria-describedby="url-help url-error">
|
|||
|
|
<span class="help" id="url-help">仅为原型假地址;本页不会向该地址发请求。</span><span class="error" id="url-error"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="field full">
|
|||
|
|
<label for="device-token">设备凭据 <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="device-token" name="device_token" type="password" value="prototype-fake-token" required autocomplete="off" aria-describedby="token-help token-error">
|
|||
|
|
<span class="help" id="token-help">仅为假值。生产实现不得把凭据写入日志、URL 或普通错误信息。</span><span class="error" id="token-error"></span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="card" aria-labelledby="poll-title">
|
|||
|
|
<div class="card-header"><h2 id="poll-title">轮询与超时</h2><p>以下均是原型示例值,最终默认值仍待实现和真机观察确认</p></div>
|
|||
|
|
<div class="card-body fields">
|
|||
|
|
<div class="field">
|
|||
|
|
<label for="poll-interval">轮询间隔(秒) <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="poll-interval" name="poll_interval" type="number" min="5" max="300" step="1" value="15" required aria-describedby="poll-help poll-error">
|
|||
|
|
<span class="help" id="poll-help">示例范围 5–300;生产值需结合风控风险确认。</span><span class="error" id="poll-error"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label for="failure-threshold">连续失败停止阈值 <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="failure-threshold" name="failure_threshold" type="number" min="1" max="10" step="1" value="3" required aria-describedby="threshold-help threshold-error">
|
|||
|
|
<span class="help" id="threshold-help">达到阈值后停止,不做无限重试。</span><span class="error" id="threshold-error"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label for="request-timeout">普通请求超时(秒) <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="request-timeout" name="request_timeout" type="number" min="1" max="120" step="1" value="10" required aria-describedby="request-help request-error">
|
|||
|
|
<span class="help" id="request-help">只用于可安全重放的普通请求。</span><span class="error" id="request-error"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label for="step-timeout">真机步骤超时(秒) <span class="required" aria-hidden="true">*</span></label>
|
|||
|
|
<input id="step-timeout" name="step_timeout" type="number" min="5" max="300" step="1" value="45" required aria-describedby="step-help step-error">
|
|||
|
|
<span class="help" id="step-help">提交围栏前后采用不同恢复协议,不以超时直接重试提交。</span><span class="error" id="step-error"></span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<aside class="side-stack" aria-label="连接与安全摘要">
|
|||
|
|
<section class="card" aria-labelledby="connection-title">
|
|||
|
|
<div class="card-header"><h2 id="connection-title">连接摘要</h2><p>当前仅为原型模拟</p></div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<dl class="summary-list">
|
|||
|
|
<div><dt>web 服务</dt><dd><span class="status warn" id="web-status">未检查</span></dd></div>
|
|||
|
|
<div><dt>ADB 设备</dt><dd><span class="status warn" id="adb-status">未检查</span></dd></div>
|
|||
|
|
<div><dt>拼多多安装</dt><dd><span class="status warn" id="app-status">未检查</span></dd></div>
|
|||
|
|
<div><dt>实际 App 版本</dt><dd id="actual-version">待真机取证</dd></div>
|
|||
|
|
<div><dt>已取证版本</dt><dd id="verified-version">待 T-103 记录</dd></div>
|
|||
|
|
</dl>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
<section class="card" aria-labelledby="boundary-title">
|
|||
|
|
<div class="card-header"><h2 id="boundary-title">检查边界</h2><p>连接检查不进入业务流程</p></div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<ul class="check-list"><li>只检查 web 可达性</li><li>只确认指定 serial 可见</li><li>只确认拼多多已安装</li><li>不打开商品或选择规格</li><li>不创建订单,更不付款</li></ul>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</aside>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<section class="form-actions" aria-label="配置操作">
|
|||
|
|
<div class="action-text" id="action-text">尚无未保存更改。连接检查不会打开拼多多。</div>
|
|||
|
|
<div class="action-buttons"><button type="button" id="check-connection">模拟连接检查</button><button type="submit" class="primary" id="save">保存假配置 <span aria-hidden="true">Ctrl+S</span></button></div>
|
|||
|
|
</section>
|
|||
|
|
</form>
|
|||
|
|
</main>
|
|||
|
|
|
|||
|
|
<dialog id="discard-dialog" aria-labelledby="discard-title">
|
|||
|
|
<div class="dialog-body"><h2 id="discard-title">放弃未保存的假配置?</h2><p>只会丢弃本页原型中的编辑,不影响任何真实设备或服务。</p></div>
|
|||
|
|
<div class="dialog-actions"><button type="button" id="stay">继续编辑</button><button type="button" class="primary" id="discard">放弃并前往采购执行</button></div>
|
|||
|
|
</dialog>
|
|||
|
|
<div class="sr-only" id="announcer" aria-live="assertive" aria-atomic="true"></div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
(() => {
|
|||
|
|
'use strict';
|
|||
|
|
const $ = (id) => document.getElementById(id);
|
|||
|
|
const form = $('settings-form');
|
|||
|
|
let dirty = false;
|
|||
|
|
const editable = [...form.querySelectorAll('input, select, button')];
|
|||
|
|
const initial = Object.fromEntries([...new FormData(form).entries()]);
|
|||
|
|
const statusCopy = {
|
|||
|
|
default:{tone:'caution',title:'尚未检查连接',text:'先确认显式 serial、ADB 和 web 地址,再运行不会打开 App 的模拟检查。'},
|
|||
|
|
invalid:{tone:'critical',title:'有 3 项需要修正',text:'输入已保留。错误位于对应字段旁;修正后再保存或检查连接。'},
|
|||
|
|
checking:{tone:'info',title:'正在模拟连接检查',text:'原型仅演示异步状态,不会执行 ADB、HTTP 或打开拼多多。'},
|
|||
|
|
connected:{tone:'success',title:'连接检查通过(模拟)',text:'显式 serial 可见、web 可达且拼多多已安装;版本匹配也仅为演示,真实值仍须 T-103 取证。'},
|
|||
|
|
web_error:{tone:'critical',title:'web 服务不可达',text:'假配置没有丢失。检查地址与网络后可重新做普通连接检查;这不代表可以重试订单提交。'},
|
|||
|
|
mismatch:{tone:'critical',title:'拼多多 App 版本失配',text:'已 fail closed:生产实现必须停止领取真机任务,由人重新取证并记录版本。'},
|
|||
|
|
saved:{tone:'success',title:'假配置已保存',text:'仅更新了浏览器内的原型快照,没有写文件、调用 API 或变更真实设备。'},
|
|||
|
|
running:{tone:'caution',title:'轮询运行中,配置已冻结',text:'先到采购执行页安全停止普通轮询。若已建立提交围栏,则必须继续调和同一 submission。'}
|
|||
|
|
};
|
|||
|
|
function setBanner(key) { const s=statusCopy[key]; $('status-banner').dataset.tone=s.tone; $('banner-title').textContent=s.title; $('banner-text').textContent=s.text; $('announcer').textContent=`${s.title}。${s.text}`; }
|
|||
|
|
function setStatus(id,text,tone) { const el=$(id); el.textContent=text; el.className=`status ${tone}`; }
|
|||
|
|
function clearErrors() { form.querySelectorAll('[aria-invalid="true"]').forEach(el=>el.removeAttribute('aria-invalid')); form.querySelectorAll('.error').forEach(el=>el.textContent=''); }
|
|||
|
|
function error(id,message) { const input=$(id), output=$(`${id.replace('failure-threshold','threshold').replace('poll-interval','poll').replace('request-timeout','request').replace('step-timeout','step').replace('web-url','url').replace('device-token','token').replace('adb-path','adb')}-error`); input.setAttribute('aria-invalid','true'); if(output) output.textContent=message; }
|
|||
|
|
function setFrozen(frozen) { editable.forEach(el=>el.disabled=frozen); $('scenario').disabled=false; $('reset').disabled=false; $('theme').disabled=false; }
|
|||
|
|
function showScenario(key) {
|
|||
|
|
if(key!=='invalid' && $('serial').value==='' && $('web-url').value==='not-a-url' && $('poll-interval').value==='1') {
|
|||
|
|
$('serial').value=initial.serial; $('web-url').value=initial.web_url; $('poll-interval').value=initial.poll_interval; dirty=false;
|
|||
|
|
}
|
|||
|
|
clearErrors(); setFrozen(false); setBanner(key);
|
|||
|
|
$('check-connection').textContent='模拟连接检查'; $('action-text').textContent=dirty?'有未保存的假配置。':'尚无未保存更改。连接检查不会打开拼多多。';
|
|||
|
|
setStatus('web-status','未检查','warn'); setStatus('adb-status','未检查','warn'); setStatus('app-status','未检查','warn');
|
|||
|
|
$('actual-version').textContent='待真机取证'; $('verified-version').textContent='待 T-103 记录';
|
|||
|
|
if(key==='invalid') {
|
|||
|
|
$('serial').value=''; $('web-url').value='not-a-url'; $('poll-interval').value='1';
|
|||
|
|
error('serial','必须显式填写精确 serial,不能留空自动选择。'); error('web-url','请输入完整的 http:// 或 https:// 地址。'); error('poll-interval','原型示例范围为 5–300 秒。');
|
|||
|
|
dirty=true; $('action-text').textContent='输入已保留;请修正 3 项错误。';
|
|||
|
|
} else if(key==='checking') {
|
|||
|
|
setFrozen(true); $('scenario').disabled=false; $('reset').disabled=false; $('theme').disabled=false; $('check-connection').textContent='检查中…'; $('action-text').textContent='等待模拟结果;没有网络请求。';
|
|||
|
|
setStatus('web-status','检查中','warn'); setStatus('adb-status','排队中','warn'); setStatus('app-status','排队中','warn');
|
|||
|
|
} else if(key==='connected') {
|
|||
|
|
setStatus('web-status','已连接(模拟)','ok'); setStatus('adb-status','指定设备可见(模拟)','ok'); setStatus('app-status','已安装;版本模拟匹配','ok');
|
|||
|
|
$('actual-version').textContent='模拟匹配;真实值待取证'; $('verified-version').textContent='待 T-103 记录真实值'; $('action-text').textContent=dirty?'连接模拟通过;仍有未保存的假配置。':'连接模拟通过;没有发出网络或 ADB 请求。';
|
|||
|
|
} else if(key==='web_error') {
|
|||
|
|
setStatus('web-status','不可达(模拟)','bad'); setStatus('adb-status','未继续检查','warn'); setStatus('app-status','未继续检查','warn');
|
|||
|
|
} else if(key==='mismatch') {
|
|||
|
|
setStatus('web-status','已连接(模拟)','ok'); setStatus('adb-status','指定设备可见(模拟)','ok'); setStatus('app-status','已安装,但版本失配','bad');
|
|||
|
|
$('actual-version').textContent='模拟:与证据版本不同'; $('verified-version').textContent='模拟:当前证据版本';
|
|||
|
|
} else if(key==='saved') {
|
|||
|
|
setStatus('web-status','已连接(模拟)','ok'); setStatus('adb-status','指定设备可见(模拟)','ok'); setStatus('app-status','已安装;版本仍待真机取证','warn'); dirty=false; $('action-text').textContent='假配置已保存。按 Ctrl+S 可再次演示保存。';
|
|||
|
|
} else if(key==='running') {
|
|||
|
|
setStatus('web-status','已连接(模拟)','ok'); setStatus('adb-status','使用中(模拟)','ok'); setStatus('app-status','运行中','warn'); setFrozen(true); $('scenario').disabled=false; $('reset').disabled=false; $('theme').disabled=false; $('action-text').textContent='运行中不能切换设备或保存参数。';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function restoreInitial() {
|
|||
|
|
for(const [name,value] of Object.entries(initial)) { const controls=form.elements.namedItem(name); if(!controls) continue; if(controls instanceof RadioNodeList) controls.value=value; else controls.value=value; }
|
|||
|
|
dirty=false; clearErrors(); $('scenario').value='default'; showScenario('default'); $('announcer').textContent='假配置已复位。';
|
|||
|
|
}
|
|||
|
|
function validate() {
|
|||
|
|
clearErrors(); const problems=[];
|
|||
|
|
if(!$('profile').value.trim()) { error('profile','请填写档案名称。'); problems.push($('profile')); }
|
|||
|
|
if(!$('serial').value.trim()) { error('serial','必须显式填写精确 serial,不能留空自动选择。'); problems.push($('serial')); }
|
|||
|
|
if(!$('adb-path').value.trim()) { error('adb-path','请选择 ADB 可执行文件。'); problems.push($('adb-path')); }
|
|||
|
|
try { const url=new URL($('web-url').value); if(!['http:','https:'].includes(url.protocol)) throw new Error(); } catch { error('web-url','请输入完整的 http:// 或 https:// 地址。'); problems.push($('web-url')); }
|
|||
|
|
if(!$('device-token').value) { error('device-token','请输入设备凭据;不要使用管理员会话凭据。'); problems.push($('device-token')); }
|
|||
|
|
const ranges=[['poll-interval',5,300,'原型示例范围为 5–300 秒。'],['failure-threshold',1,10,'原型示例范围为 1–10 次。'],['request-timeout',1,120,'原型示例范围为 1–120 秒。'],['step-timeout',5,300,'原型示例范围为 5–300 秒。']];
|
|||
|
|
for(const [id,min,max,msg] of ranges) { const n=Number($(id).value); if(!Number.isInteger(n)||n<min||n>max) { error(id,msg); problems.push($(id)); } }
|
|||
|
|
if(problems.length) { $('scenario').value='invalid'; setBanner('invalid'); $('banner-title').textContent=`有 ${problems.length} 项需要修正`; $('action-text').textContent=`输入已保留;请修正 ${problems.length} 项错误。`; problems[0].focus(); return false; }
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
form.addEventListener('input', (e) => { if(e.target.matches('input,select')) { dirty=true; $('action-text').textContent='有未保存的假配置。'; if(e.target.hasAttribute('aria-invalid')) { e.target.removeAttribute('aria-invalid'); const err=e.target.closest('.field')?.querySelector('.error'); if(err) err.textContent=''; } } });
|
|||
|
|
form.addEventListener('submit', (e) => { e.preventDefault(); if(validate()) { $('scenario').value='saved'; showScenario('saved'); } });
|
|||
|
|
$('check-connection').addEventListener('click', () => { if(!validate()) return; $('scenario').value='checking'; showScenario('checking'); window.setTimeout(()=>{ $('scenario').value='connected'; showScenario('connected'); },650); });
|
|||
|
|
$('scenario').addEventListener('change', (e) => showScenario(e.target.value));
|
|||
|
|
$('reset').addEventListener('click', restoreInitial);
|
|||
|
|
$('theme').addEventListener('change', (e) => { document.documentElement.dataset.theme=e.target.value; $('announcer').textContent=`主题已切换为${e.target.options[e.target.selectedIndex].text}`; });
|
|||
|
|
$('execution-link').addEventListener('click', (e) => { if(!dirty) return; e.preventDefault(); $('discard-dialog').showModal(); });
|
|||
|
|
$('stay').addEventListener('click', () => $('discard-dialog').close());
|
|||
|
|
$('discard').addEventListener('click', () => { dirty=false; location.href='desk-execution.html'; });
|
|||
|
|
window.addEventListener('keydown', (e) => { if(e.ctrlKey && e.key.toLowerCase()==='s') { e.preventDefault(); form.requestSubmit(); } });
|
|||
|
|
showScenario('default');
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|