Files
cmbuyer/docs/design/desk-device-settings.html
T

358 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN" 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>