Files
auto_control/templates/admin/monitor.html
T

778 lines
51 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">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备自动化后台</title>
<style>
/* ========== 仪表控制台主题(Instrument Console) ==========
自托管字体:Bricolage Grotesque(展示/数字)+ IBM Plex Mono(数据/串号)
配色:暖纸底 + 墨色文字 + 深青主色;状态色 = 绿/红/琥珀/靛 */
@font-face{font-family:'Bricolage Grotesque';font-style:normal;font-weight:500;font-display:swap;src:url('/static/fonts/bricolage-500.woff2') format('woff2')}
@font-face{font-family:'Bricolage Grotesque';font-style:normal;font-weight:700;font-display:swap;src:url('/static/fonts/bricolage-700.woff2') format('woff2')}
@font-face{font-family:'Bricolage Grotesque';font-style:normal;font-weight:800;font-display:swap;src:url('/static/fonts/bricolage-800.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url('/static/fonts/plexmono-400.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url('/static/fonts/plexmono-500.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:600;font-display:swap;src:url('/static/fonts/plexmono-600.woff2') format('woff2')}
:root{
--primary:#0f766e; --primary-dark:#115e59; --primary-soft:#e6f4f1;
--success:#16a34a; --success-soft:#e8f7ee;
--danger:#dc2626; --danger-soft:#fdecec;
--warning:#d97706; --warning-soft:#fdf3e3;
--info:#4f46e5; --info-soft:#ecebfb;
--bg:#f5f4ef; --card:#ffffff; --border:#e6e3da; --border-strong:#d8d4c8;
--text:#1b1e24; --text-muted:#69707c; --text-light:#9aa1ab;
--ink:#14161a; --ink-2:#1e2127; --ink-line:#2a2e37;
--display:'Bricolage Grotesque','PingFang SC','Microsoft YaHei',sans-serif;
--mono:'IBM Plex Mono','SF Mono',Menlo,Consolas,monospace;
--body:-apple-system,'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;
--shadow-sm:0 1px 2px rgba(27,30,36,.05),0 1px 3px rgba(27,30,36,.06);
--shadow-md:0 4px 14px rgba(27,30,36,.08),0 2px 4px rgba(27,30,36,.05);
--shadow-lg:0 20px 50px rgba(20,22,26,.18);
--radius:10px;
}
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-font-smoothing:antialiased}
body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14px;line-height:1.5}
::-webkit-scrollbar{width:9px;height:9px}
::-webkit-scrollbar-thumb{background:#c9c5b9;border-radius:5px;border:2px solid var(--bg)}
::-webkit-scrollbar-thumb:hover{background:#b0ab9d}
::-webkit-scrollbar-track{background:transparent}
/* 顶部导航:墨色 + 顶部青线 + 品牌脉冲点 */
.navbar{background:linear-gradient(180deg,#1a1d23 0%,#14161a 100%);border-top:3px solid var(--primary);padding:0 28px;display:flex;align-items:center;height:58px;box-shadow:0 2px 10px rgba(0,0,0,.2);position:sticky;top:0;z-index:500}
.navbar .brand{color:#f3f4f6;font-family:var(--display);font-weight:700;font-size:16px;letter-spacing:.3px;margin-right:34px;white-space:nowrap;display:flex;align-items:center;gap:9px}
.brand-dot{width:9px;height:9px;border-radius:50%;background:var(--primary);box-shadow:0 0 0 0 rgba(15,118,110,.6);animation:brandPulse 2.4s infinite}
@keyframes brandPulse{0%{box-shadow:0 0 0 0 rgba(15,118,110,.45)}70%{box-shadow:0 0 0 8px rgba(15,118,110,0)}100%{box-shadow:0 0 0 0 rgba(15,118,110,0)}}
.navbar .tabs{display:flex;gap:2px;flex:1;align-self:stretch}
.navbar .tab{position:relative;padding:0 18px;color:#9aa3af;font-size:14px;font-weight:500;cursor:pointer;border:none;background:none;transition:color .15s;letter-spacing:.2px}
.navbar .tab::after{content:"";position:absolute;left:14px;right:14px;bottom:0;height:2px;border-radius:2px;background:var(--primary);transform:scaleX(0);transition:transform .18s}
.navbar .tab:hover{color:#e5e7eb}
.navbar .tab.active{color:#fff}
.navbar .tab.active::after{transform:scaleX(1)}
.navbar .user-info{color:#8a93a0;font-size:13px;display:flex;align-items:center;gap:14px}
.navbar .user-info a{color:#c2c9d3;text-decoration:none;font-size:13px;transition:color .15s}
.navbar .user-info a:hover{color:#fff}
/* 内容区 */
.content{max-width:1600px;margin:0 auto;padding:26px 30px 60px}
.tab-panel{display:none}
.tab-panel.active{display:block;animation:panelIn .22s ease-out}
@keyframes panelIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.page-title{font-family:var(--display);font-size:21px;font-weight:800;letter-spacing:.2px;color:var(--text);margin:0 0 4px}
.page-sub{font-size:13px;color:var(--text-light);margin-bottom:22px}
/* 统计卡片:左侧色条 + 等宽数字 */
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:14px;margin-bottom:26px}
.stat-card{background:var(--card);border-radius:var(--radius);padding:15px 18px;box-shadow:var(--shadow-sm);border:1px solid var(--border);position:relative;overflow:hidden;transition:transform .15s,box-shadow .15s}
.stat-card:hover{transform:translateY(-2px);box-shadow:var(--shadow-md)}
.stat-card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--accent,#0f766e);border-radius:0 3px 3px 0}
.stat-card .stat-num{font-family:var(--mono);font-size:25px;font-weight:600;line-height:1.1;margin-bottom:5px;color:var(--accent,#0f766e);font-variant-numeric:tabular-nums}
.stat-card .stat-lbl{font-size:12px;color:var(--text-muted);font-weight:500;letter-spacing:.2px}
/* 工具栏 */
.toolbar{display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap}
.toolbar .hint{color:var(--text-light);font-size:12px;margin-left:auto}
/* 按钮 */
.btn{border-radius:8px;font-weight:500;border:1px solid var(--border-strong);padding:7px 16px;font-size:13px;transition:all .15s;cursor:pointer;background:var(--card);color:var(--text);box-shadow:var(--shadow-sm)}
.btn:hover{background:#faf9f5;border-color:#c4bfb2;transform:translateY(-1px)}
.btn:active{transform:none}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
.btn-primary{background:var(--primary);border-color:var(--primary);color:#fff;box-shadow:0 1px 4px rgba(15,118,110,.35)}
.btn-primary:hover{background:var(--primary-dark);border-color:var(--primary-dark)}
.btn-danger{background:var(--danger);border-color:var(--danger);color:#fff;box-shadow:0 1px 4px rgba(220,38,38,.3)}
.btn-danger:hover{background:#b91c1c;border-color:#b91c1c}
.btn-success{background:var(--success);border-color:var(--success);color:#fff}
.btn-success:hover{background:#15803d;border-color:#15803d}
.btn-sm{padding:4px 12px;font-size:12px}
.btn-xs{padding:3px 10px;font-size:12px;border-radius:6px}
/* 表单 */
.form-control{background:#fff;border:1px solid var(--border-strong);border-radius:8px;padding:7px 12px;font-size:13px;color:var(--text);transition:border-color .15s,box-shadow .15s;outline:none;font-family:var(--body)}
.form-control:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(15,118,110,.12)}
.form-control::placeholder{color:var(--text-light)}
.form-group{margin-bottom:12px}
.form-group label{display:block;font-size:12px;font-weight:600;color:var(--text-muted);margin-bottom:5px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.form-check{display:flex;align-items:center;gap:7px;cursor:pointer;font-size:13px}
.form-check input{width:15px;height:15px;accent-color:var(--primary);cursor:pointer}
/* 通用列表分页栏 */
.list-pager{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:10px 0 22px}
.list-pager .lp-total{font-size:12px;color:var(--text-light);margin-right:auto}
.list-search{width:240px}
/* 控制台/日志(墨色终端) */
.adb-console{background:var(--ink);color:#d5dae1;font-family:var(--mono);font-size:12.5px;padding:14px;border-radius:10px;min-height:200px;max-height:420px;overflow:auto;white-space:pre-wrap;word-break:break-all;box-shadow:inset 0 1px 6px rgba(0,0,0,.4);border:1px solid var(--ink-line)}
.log-content{background:var(--ink);color:#d5dae1;border-radius:10px;padding:16px;font-family:var(--mono);font-size:12.5px;line-height:1.65;max-height:600px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;border:1px solid var(--ink-line);box-shadow:inset 0 1px 6px rgba(0,0,0,.4)}
.log-content .log-err{color:#f87171}
.log-content .log-warn{color:#fbbf24}
.log-content .log-info{color:#67e8f9}
.log-toolbar{display:flex;gap:10px;margin-bottom:12px;align-items:center}
/* 区块标题 */
.section-title{font-family:var(--display);font-size:15px;font-weight:700;color:var(--text);margin:0 0 12px;padding-bottom:8px;border-bottom:2px solid var(--primary);display:inline-block;letter-spacing:.2px}
/* 页内子分栏 */
.sub-tabs{display:flex;gap:2px;margin-bottom:18px;border-bottom:1px solid var(--border-strong)}
.sub-tab{padding:10px 16px;font-size:13px;font-weight:600;color:var(--text-muted);background:none;border:none;cursor:pointer;border-radius:8px 8px 0 0;transition:all .15s;position:relative}
.sub-tab:hover{color:var(--text);background:#efede6}
.sub-tab.active{color:var(--primary)}
.sub-tab.active::after{content:"";position:absolute;left:10px;right:10px;bottom:-1px;height:2px;background:var(--primary);border-radius:2px}
.sub-panel{display:none}
.sub-panel.active{display:block;animation:panelIn .2s ease-out}
/* 表格 */
.table{background:var(--card);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm);border:1px solid var(--border);width:100%;border-collapse:collapse;margin-bottom:26px}
.table thead th{background:#faf9f5;border-bottom:1px solid var(--border-strong);font-weight:600;color:var(--text-muted);font-size:12px;letter-spacing:.4px;padding:12px 14px;text-align:left;white-space:nowrap}
.table tbody td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle;font-size:13px}
.table tbody tr:last-child td{border-bottom:none}
.table tbody tr{transition:background .1s}
.table tbody tr:hover td{background:#faf9f4}
.table thead th.sortable{cursor:pointer;user-select:none}
.table thead th.sortable:hover{background:#f3f1e9}
.table thead th.sortable::after{content:" ⇅";color:#c6c1b4;font-weight:400;font-size:11px}
.table thead th.sortable.sort-asc::after{content:" ↑";color:var(--primary);font-weight:700}
.table thead th.sortable.sort-desc::after{content:" ↓";color:var(--primary);font-weight:700}
.table tbody tr.row-checked td{background:var(--primary-soft)}
.table .col-cb{width:38px;text-align:center}
.table .col-cb input,.form-check input[type=checkbox],.device-list input{width:15px;height:15px;accent-color:var(--primary);cursor:pointer}
/* 徽章(柔和底 + 深色字) */
.label{padding:3px 10px;border-radius:999px;font-size:11px;font-weight:600;display:inline-block;line-height:1.5;letter-spacing:.2px}
.label-success{background:var(--success-soft);color:#15803d}
.label-default{background:#f1efe8;color:var(--text-muted)}
.label-warning{background:var(--warning-soft);color:#b45309}
.label-primary{background:var(--primary-soft);color:#0f766e}
.label-info{background:var(--info-soft);color:#4338ca}
.label-danger{background:var(--danger-soft);color:#b91c1c}
/* 进度条 */
.progress{background:#e9e6dd;border-radius:99px;height:6px;overflow:hidden;display:inline-block;vertical-align:middle;width:70px}
.progress-bar{background:var(--primary);height:100%;border-radius:99px;transition:width .3s}
/* 圆点 */
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}
.dot-on{background:var(--success);box-shadow:0 0 6px rgba(22,163,74,.55)}
.dot-off{background:var(--danger);opacity:.55}
/* 任务卡片 */
.jobs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(350px,1fr));gap:14px}
.job-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:15px 16px;box-shadow:var(--shadow-sm);transition:all .18s;position:relative}
.job-card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px)}
.job-card.running{border-color:rgba(22,163,74,.55);box-shadow:0 0 0 1px rgba(22,163,74,.35),var(--shadow-sm)}
.job-card .job-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:9px}
.job-card .job-name{font-size:14px;font-weight:600;color:var(--text)}
.job-card .job-type{font-size:11px;color:var(--text-muted);background:#f1efe8;padding:2px 8px;border-radius:99px;margin-top:3px;display:inline-block;font-weight:500}
.job-card .job-body{font-size:12px;color:var(--text-muted);margin-bottom:11px;line-height:1.65}
.job-card .job-body .kv{display:inline-block;background:#faf9f5;border:1px solid var(--border);border-radius:6px;padding:1px 7px;margin:2px 3px 0 0;font-family:var(--mono);font-size:11px}
.job-card .job-actions{display:flex;gap:6px;flex-wrap:wrap}
/* 空状态 / 辅助文本 */
.empty{text-align:center;padding:42px;color:var(--text-light);font-size:13px}
.text-muted{color:var(--text-light)!important}
.err-text{color:var(--danger);font-size:12px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pulse{animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
.help{font-size:12px;color:var(--text-light);line-height:1.7;background:#faf9f4;border:1px dashed var(--border-strong);border-radius:8px;padding:10px 12px;margin:10px 0}
.help code{font-family:var(--mono);background:#f1efe8;padding:1px 5px;border-radius:4px;font-size:11px;color:var(--primary-dark)}
/* 模态框 */
.modal-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(20,22,26,.5);backdrop-filter:blur(2px);z-index:1000;justify-content:center;align-items:flex-start;padding:40px 20px;overflow-y:auto}
.modal-overlay.show{display:flex}
.modal{background:var(--card);border-radius:14px;box-shadow:var(--shadow-lg);width:100%;max-width:640px;margin-bottom:40px;border:1px solid var(--border);animation:modalIn .18s ease-out}
@keyframes modalIn{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}
.modal-header{padding:18px 24px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
.modal-header h3{font-size:17px;font-weight:700;color:var(--text);font-family:var(--display)}
.modal-header .btn{background:none;border:none;font-size:20px;color:var(--text-light);padding:0 6px;box-shadow:none}
.modal-header .btn:hover{color:var(--text)}
.modal-body{padding:20px 24px}
.modal-footer{padding:14px 24px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px}
.shot-modal{max-width:none;width:min(440px,95vw)}
.wide-modal{max-width:1000px}
.shot-modal .modal-body{text-align:center;padding:14px}
.shot-img{max-width:100%;max-height:calc(100vh - 240px);width:auto;height:auto;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.2);background:var(--ink);min-height:200px;display:block;margin:0 auto}
.shot-loading{color:var(--text-light);padding:60px 0;font-size:13px}
.shot-err{color:var(--danger);padding:40px 0;font-size:13px;white-space:pre-wrap;word-break:break-all}
.shot-info{font-size:12px;color:var(--text-muted);margin-top:10px}
.shot-toolbar{display:flex;gap:8px;justify-content:center;align-items:center;margin-top:12px;flex-wrap:wrap}
.shot-toolbar .auto-label{font-size:12px;color:var(--text-muted);display:flex;align-items:center;gap:4px}
/* Toast */
.toast{position:fixed;top:20px;right:20px;z-index:2000;padding:12px 20px;border-radius:10px;font-size:14px;font-weight:500;box-shadow:var(--shadow-lg);display:none;max-width:400px;border-left:4px solid;animation:toastIn .18s ease-out}
.toast.show{display:block}
@keyframes toastIn{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}
.toast-success{background:#f0faf4;color:#166534;border-color:var(--success)}
.toast-error{background:#fef2f2;color:#991b1b;border-color:var(--danger)}
/* 设备多选列表 */
.device-list{max-height:200px;overflow-y:auto;border:1px solid var(--border-strong);border-radius:8px;padding:8px;background:#fff}
.device-list label{display:flex;align-items:center;gap:6px;padding:4px 8px;font-size:13px;font-weight:400;cursor:pointer;border-radius:6px}
.device-list label:hover{background:#f5f3ec}
/* 步骤编辑器 */
.step-editor{display:none;border:1px solid var(--border);border-radius:12px;overflow:hidden;margin-top:8px;background:#fff;box-shadow:var(--shadow-sm)}
.step-editor.show{display:block}
.step-editor .se-head{background:#faf9f5;padding:10px 14px;font-weight:600;font-size:13px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}
.step-editor .se-body{display:grid;grid-template-columns:200px 1fr;min-height:280px}
.step-editor .se-lib{border-right:1px solid var(--border);padding:8px;background:#faf9f4;overflow-y:auto;max-height:520px}
.step-editor .se-lib .lib-title{font-size:11px;color:var(--text-light);font-weight:600;margin-bottom:6px;text-transform:uppercase;letter-spacing:.6px}
.step-editor .se-lib .lib-item{padding:6px 8px;border-radius:6px;cursor:grab;font-size:12px;color:var(--text);display:flex;align-items:center;gap:6px;margin-bottom:2px;transition:background .1s;white-space:nowrap;overflow:hidden}
.step-editor .se-lib .lib-item span:last-child{overflow:hidden;text-overflow:ellipsis}
.step-editor .se-lib .lib-item:hover{background:#ece9e0}
.step-editor .se-lib .lib-item:active{cursor:grabbing}
.step-editor .se-lib .lib-item .ico{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border-radius:5px;font-size:11px;flex-shrink:0}
.step-editor .se-lib .lib-section{margin-top:8px;padding-top:6px;border-top:1px solid var(--border)}
.step-editor .se-lib .lib-item.custom .ico{background:#7c3aed}
.step-editor .se-lib .lib-item.custom .del-btn{margin-left:auto;background:none;border:none;color:var(--text-light);cursor:pointer;font-size:13px;padding:0 2px}
.step-editor .se-lib .lib-item.custom .del-btn:hover{color:var(--danger)}
.step-editor .se-canvas{padding:8px;overflow-y:auto;max-height:480px}
.step-editor .se-empty{text-align:center;color:var(--text-light);padding:40px 20px;font-size:13px}
.step-editor .se-empty .hint{font-size:11px;margin-top:4px}
.se-toolbar{display:flex;gap:6px;padding:6px 8px;border-bottom:1px solid var(--border);background:#faf9f5}
.se-toolbar .btn{padding:4px 10px;font-size:12px}
.step-card{background:#fff;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;transition:box-shadow .15s}
.step-card.drag-over{border-color:var(--primary);box-shadow:0 0 0 2px rgba(15,118,110,.18)}
.step-card.dragging{opacity:.5}
.step-card .sc-head{display:flex;align-items:center;gap:8px;padding:7px 10px;cursor:grab}
.step-card .sc-head:active{cursor:grabbing}
.step-card .sc-ico{width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border-radius:6px;font-size:12px;flex-shrink:0}
.step-card .sc-label{flex:1;font-size:13px;font-weight:500;color:var(--text)}
.step-card .sc-type{font-size:11px;color:var(--text-light);background:#f1efe8;padding:1px 6px;border-radius:4px}
.step-card .sc-actions{display:flex;gap:2px}
.step-card .sc-actions button{background:none;border:none;color:var(--text-light);cursor:pointer;padding:2px 5px;font-size:14px;border-radius:4px;line-height:1}
.step-card .sc-actions button:hover{background:#f1efe8;color:var(--text)}
.step-card .sc-body{display:none;padding:8px 10px;border-top:1px solid var(--border);background:#faf9f4}
.step-card.expanded .sc-body{display:block}
.step-card .sc-body .form-row{grid-template-columns:1fr 1fr 1fr;gap:8px}
.step-card .sc-body .form-group{margin-bottom:8px}
.step-card .sc-body .form-group label{font-size:11px;margin-bottom:2px}
.step-card .sc-body .form-control{padding:5px 8px;font-size:12px}
.step-card.loop-block{border-left:3px solid var(--success)}
.step-card.loop-block .sc-ico{background:var(--success)}
.step-card.group-block{border-left:3px solid #7c3aed}
.step-card.group-block .sc-ico{background:#7c3aed}
.step-card.if-block{border-left:3px solid var(--warning)}
.step-card.if-block .sc-ico{background:var(--warning)}
.step-card.selected{border-color:var(--primary);box-shadow:0 0 0 2px rgba(15,118,110,.22)}
.step-children{margin-left:20px;margin-top:6px;border-left:2px dashed var(--border-strong);padding-left:8px}
.if-branch{margin-top:6px}
.if-branch .branch-title{font-size:11px;font-weight:600;padding:3px 10px;border-radius:4px;margin-bottom:4px;display:inline-block}
.if-branch .branch-title.then{color:#15803d;background:var(--success-soft)}
.if-branch .branch-title.else{color:#b91c1c;background:var(--danger-soft)}
.step-children.drag-over{border-left-color:var(--primary);background:rgba(15,118,110,.06);border-radius:6px}
#se-canvas.drag-over{outline:2px dashed var(--primary);outline-offset:2px;border-radius:8px}
.sel-picker{display:flex;gap:4px;align-items:center}
.sel-picker input{flex:1}
.sel-picker .btn-pick{padding:5px 8px;font-size:11px;white-space:nowrap}
/* 元素抓取弹窗 */
.el-device-grid{padding:16px;overflow-y:auto;flex:1}
.el-device-grid .egd-title{font-size:14px;font-weight:600;margin-bottom:12px;color:var(--text)}
.el-device-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:12px;margin-bottom:8px;box-shadow:var(--shadow-sm)}
.el-device-card:hover{border-color:var(--primary);box-shadow:var(--shadow-md);transform:translateY(-1px)}
.el-device-card .edc-icon{width:36px;height:36px;background:#f1efe8;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.el-device-card .edc-info{flex:1}
.el-device-card .edc-serial{font-size:13px;font-weight:600;color:var(--text);font-family:var(--mono)}
.el-device-card .edc-model{font-size:12px;color:var(--text-light);margin-top:2px}
.el-device-card .edc-status{font-size:11px;padding:2px 10px;border-radius:99px;background:var(--success-soft);color:#15803d;flex-shrink:0;font-weight:600}
.el-picker-main{display:flex;flex:1;overflow:hidden}
.el-shot-wrap{flex:0 0 320px;border-right:1px solid var(--border);background:var(--ink);display:flex;flex-direction:column}
.el-shot-toolbar{padding:6px 10px;background:var(--ink-2);display:flex;gap:6px;align-items:center;border-bottom:1px solid var(--ink-line)}
.el-shot-toolbar .btn{padding:3px 10px;font-size:12px;background:#39414d;color:#e5e7eb;border:none;box-shadow:none}
.el-shot-toolbar .btn:hover{background:#4b5563}
.el-shot-toolbar .el-shot-info{color:#9aa3af;font-size:11px;flex:1}
.el-shot-container{flex:1;overflow:auto;display:flex;align-items:flex-start;justify-content:center;padding:8px;position:relative}
.el-shot-container img{max-width:100%;height:auto;border-radius:4px;display:block}
.el-shot-container .el-shot-loading{color:#8a93a0;font-size:13px;padding:40px}
.el-shot-overlay{position:absolute;border:1px solid rgba(103,232,249,.35);background:transparent;cursor:pointer;border-radius:2px;transition:all .1s}
.el-shot-overlay:hover{border-color:#67e8f9;background:rgba(103,232,249,.18)}
.el-shot-overlay.active{border:2px solid #f87171;background:rgba(248,113,113,.22)}
.el-list-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden}
.el-picker-search{width:100%;padding:8px 12px;border:none;border-bottom:1px solid var(--border);font-size:13px;outline:none;font-family:var(--body)}
.el-picker-list{flex:1;overflow-y:auto}
.el-picker-item{padding:8px 12px;border-bottom:1px solid var(--border);cursor:pointer;font-size:12px;display:flex;gap:8px;align-items:flex-start}
.el-picker-item:hover{background:var(--primary-soft)}
.el-picker-item.active{background:var(--primary-soft);border-left:3px solid var(--primary)}
.el-picker-item:last-child{border-bottom:none}
.el-picker-item .ep-depth{color:var(--text-light);font-family:var(--mono);flex-shrink:0;width:28px}
.el-picker-item .ep-info{flex:1;min-width:0}
.el-picker-item .ep-info .ep-name{color:var(--text);font-weight:500;word-break:break-all}
.el-picker-item .ep-info .ep-attrs{color:var(--text-light);font-size:11px;margin-top:2px;word-break:break-all}
.el-picker-item .ep-info .ep-attrs span{margin-right:8px}
.el-picker-item .ep-sel{font-size:10px;color:#fff;background:var(--primary);padding:1px 7px;border-radius:99px;flex-shrink:0;font-weight:600}
</style>
</head>
<body>
<!-- 顶部导航 -->
<nav class="navbar">
<div class="brand"><span class="brand-dot"></span>设备自动化后台</div>
<div class="tabs">
<button class="tab active" data-tab="monitor" onclick="showTab('monitor')">监控</button>
<button class="tab" data-tab="tasks" onclick="showTab('tasks')">任务</button>
<button class="tab" data-tab="groups" onclick="showTab('groups')">分组</button>
<button class="tab" data-tab="logs" onclick="showTab('logs')" data-perm="logs">日志</button>
<button class="tab" data-tab="users" onclick="showTab('users')" data-perm="admin">用户</button>
<button class="tab" data-tab="tools" onclick="showTab('tools')" data-perm="admin">工具</button>
</div>
<div class="user-info">
<span id="me-username">...</span>
<a href="/wall" target="_blank" title="监控大屏(全屏,挂墙/电视用)">📺 大屏</a>
<a href="/logout">退出</a>
</div>
</nav>
<div class="content">
<!-- ========== Tab 1: 监控 ========== -->
<div id="tab-monitor" class="tab-panel active">
<div class="page-title">设备状态监控</div>
<div class="page-sub">实时展示所有设备状态、当前执行任务及进度</div>
<div id="stats" class="stats-row"></div>
<div class="section-title">设备列表</div>
<div class="toolbar">
<button class="btn btn-danger" onclick="apiAction('/api/stop_all')" data-perm="devices">停止全部</button>
<button class="btn btn-danger" id="btn-stop-selected" onclick="stopSelectedDevices()" title="停止所有勾选的运行中设备" data-perm="devices">停止选中</button>
<button class="btn btn-danger" onclick="clearAllErrors()" title="清除所有异常/失败设备的错误状态(运行中/等待重试的跳过)" data-perm="devices">清除全部异常</button>
<button class="btn btn-primary" id="btn-scan-fg" onclick="scanForeground()" data-perm="devices">扫描前台App</button>
<button class="btn" onclick="screenAllDevices('on')" data-perm="devices" title="所有在线设备亮屏并解锁">一键亮屏</button>
<button class="btn" onclick="screenAllDevices('off')" data-perm="devices" title="所有在线设备息屏(运行中任务会中断)">一键息屏</button>
<button class="btn" onclick="loadMonitor()">刷新</button>
<input class="form-control list-search" id="search-devices" placeholder="搜索设备/型号/App/任务...">
<span class="hint"><span id="fg-status"></span> · 每 5 秒自动刷新 · <span id="srvTime"></span> · <span id="sel-count">未选中</span></span>
</div>
<table class="table">
<thead><tr>
<th class="col-cb"><input type="checkbox" id="cb-all" onclick="toggleAllDevices(this.checked)" title="全选/取消全选"></th>
<th data-sort="serial" onclick="_lpSort('devices','serial')">设备</th>
<th data-sort="model" onclick="_lpSort('devices','model')">型号</th>
<th data-sort="worker_status" onclick="_lpSort('devices','worker_status')">任务状态</th>
<th data-sort="foreground_app" onclick="_lpSort('devices','foreground_app')">前台App</th>
<th data-sort="progress" onclick="_lpSort('devices','progress')">进度</th>
<th data-sort="task_job" onclick="_lpSort('devices','task_job')">当前任务</th>
<th data-sort="current_action" onclick="_lpSort('devices','current_action')">当前动作</th>
<th data-sort="last_error" onclick="_lpSort('devices','last_error')">最近错误</th>
<th>操作</th>
</tr></thead>
<tbody id="tb-devices"></tbody>
</table>
<div class="list-pager" id="pager-devices"></div>
<!-- 异常设备汇总 -->
<div class="section-title" style="margin-top:8px">异常汇总 <span id="summary-badge" style="font-size:12px;font-weight:400;color:#94a3b8"></span></div>
<div id="summary-panel" style="display:none;margin-bottom:12px">
<table class="table">
<thead><tr><th>设备</th><th>状态</th><th>任务</th><th>重试</th><th>错误信息</th></tr></thead>
<tbody id="tb-summary"></tbody>
</table>
</div>
<div id="summary-empty" style="display:none" class="text-muted">无异常设备</div>
<div class="section-title" style="margin-top:8px">任务运行概况</div>
<div id="jobs-grid" class="jobs-grid"></div>
</div>
<!-- ========== Tab 2: 任务 ========== -->
<div id="tab-tasks" class="tab-panel">
<div class="page-title">任务</div>
<div class="page-sub">任务计划定时调度与自定义动作复用管理</div>
<div class="sub-tabs">
<button class="sub-tab active" data-sub="plan" onclick="showSubTab('tasks','plan')">任务计划</button>
<button class="sub-tab" data-sub="actions" onclick="showSubTab('tasks','actions')">自定义动作</button>
</div>
<!-- 子分栏 1:任务计划 -->
<div class="sub-panel active" id="tasks-sub-plan">
<div class="toolbar">
<button class="btn btn-primary" onclick="openTaskModal()" data-perm="tasks">新建任务</button>
<button class="btn" onclick="loadTasks()">刷新</button>
<input class="form-control list-search" id="search-tasks" placeholder="搜索任务名/类型/ID...">
</div>
<table class="table">
<thead><tr>
<th data-sort="id" onclick="_lpSort('tasks','id')">ID</th><th data-sort="name" onclick="_lpSort('tasks','name')">任务名</th><th data-sort="task_type" onclick="_lpSort('tasks','task_type')">类型</th><th data-sort="enabled" onclick="_lpSort('tasks','enabled')">启用</th><th>目标</th><th>调度</th><th data-sort="next_run" onclick="_lpSort('tasks','next_run')">下次运行</th><th>重试</th><th>操作</th>
</tr></thead>
<tbody id="tb-tasks"></tbody>
</table>
<div class="list-pager" id="pager-tasks"></div>
</div>
<!-- 子分栏 2:自定义动作 -->
<div class="sub-panel" id="tasks-sub-actions">
<div class="toolbar">
<button class="btn btn-primary" onclick="openActionModal()" data-perm="tasks">新建动作</button>
<button class="btn" onclick="loadCustomActions()">刷新</button>
<input class="form-control list-search" id="search-actions" placeholder="搜索动作名...">
</div>
<div class="help">把常用步骤打包成可复用的动作,拖入任务步骤编辑器使用;也可在任务编辑器里"☑选中步骤→打包"生成</div>
<table class="table">
<thead><tr><th>图标</th><th data-sort="name" onclick="_lpSort('actions','name')">动作名</th><th data-sort="steps" onclick="_lpSort('actions','steps')">步骤数</th><th data-sort="created_at" onclick="_lpSort('actions','created_at')">创建时间</th><th>操作</th></tr></thead>
<tbody id="tb-actions"></tbody>
</table>
<div class="list-pager" id="pager-actions"></div>
</div>
</div>
<!-- ========== Tab 3: 分组 ========== -->
<div id="tab-groups" class="tab-panel">
<div class="page-title">设备分组管理</div>
<div class="page-sub">管理设备分组,用于任务按分组执行</div>
<div class="toolbar">
<button class="btn btn-primary" onclick="openGroupModal()" data-perm="tasks">新建分组</button>
<button class="btn" onclick="loadGroups()">刷新</button>
<input class="form-control list-search" id="search-groups" placeholder="搜索分组名/描述...">
</div>
<table class="table">
<thead><tr>
<th data-sort="name" onclick="_lpSort('groups','name')">分组名</th><th>描述</th><th data-sort="count" onclick="_lpSort('groups','count')">设备数</th><th>设备列表</th><th>操作</th>
</tr></thead>
<tbody id="tb-groups"></tbody>
</table>
<div class="list-pager" id="pager-groups"></div>
</div>
<!-- ========== Tab 4: 日志 ========== -->
<div id="tab-logs" class="tab-panel">
<div class="page-title">日志查看</div>
<div class="page-sub">查看系统各模块运行日志</div>
<div class="log-toolbar">
<select id="log-file" class="form-control" style="width:auto" onchange="loadLogs()">
<option value="core.log">核心日志 (core.log)</option>
<option value="task.log">任务日志 (task.log)</option>
<option value="web.log">Web日志 (web.log)</option>
<option value="action.log">操作日志 (action.log)</option>
</select>
<select id="log-lines" class="form-control" style="width:auto" onchange="loadLogs()">
<option value="100">最近 100 行</option>
<option value="300" selected>最近 300 行</option>
<option value="500">最近 500 行</option>
<option value="1000">最近 1000 行</option>
</select>
<label class="form-check"><input type="checkbox" id="log-auto" checked onchange="toggleLogAuto()"> 自动刷新</label>
<button class="btn" onclick="loadLogs()">刷新</button>
<span class="hint" id="log-hint"></span>
</div>
<div id="log-content" class="log-content"></div>
</div>
<!-- ========== Tab 5: 用户 ========== -->
<div id="tab-users" class="tab-panel">
<div class="page-title">用户管理</div>
<div class="page-sub">管理后台登录用户</div>
<div class="toolbar">
<button class="btn btn-primary" onclick="openUserModal()">新建用户</button>
<button class="btn" onclick="loadUsers()">刷新</button>
<input class="form-control list-search" id="search-users" placeholder="搜索用户名...">
</div>
<table class="table">
<thead><tr>
<th data-sort="id" onclick="_lpSort('users','id')">ID</th><th data-sort="username" onclick="_lpSort('users','username')">用户名</th><th data-sort="is_admin" onclick="_lpSort('users','is_admin')">管理员</th><th>权限</th><th>操作</th>
</tr></thead>
<tbody id="tb-users"></tbody>
</table>
<div class="list-pager" id="pager-users"></div>
</div>
<!-- ========== Tab 7: 维护(仅管理员) ========== -->
<!-- ========== Tab 8: 工具(仅管理员) ========== -->
<div id="tab-tools" class="tab-panel">
<div class="page-title">工具</div>
<div class="page-sub">管理员专用:设备工具与应用管理集合</div>
<div class="sub-tabs">
<button class="sub-tab active" data-sub="clipboard" onclick="showSubTab('tools','clipboard')">剪贴板注入</button>
<button class="sub-tab" data-sub="adb" onclick="showSubTab('tools','adb')">adb 远程终端</button>
<button class="sub-tab" data-sub="ts" onclick="showSubTab('tools','ts')">Tailscale 管理</button>
<button class="sub-tab" data-sub="apks" onclick="showSubTab('tools','apks')">应用管理</button>
<button class="sub-tab" data-sub="appver" onclick="showSubTab('tools','appver')">应用版本管理</button>
<button class="sub-tab" data-sub="devapps" onclick="showSubTab('tools','devapps')">设备已装应用</button>
<button class="sub-tab" data-sub="devpool" onclick="showSubTab('tools','devpool')">设备池管理</button>
<button class="sub-tab" data-sub="screen" onclick="showSubTab('tools','screen')">远程看屏</button>
</div>
<!-- 子分栏 1:剪贴板注入 -->
<div class="sub-panel active" id="tools-sub-clipboard">
<div class="toolbar">
<button class="btn" onclick="loadToolsDevices(true)">刷新设备</button>
<span id="clip-status" class="text-muted"></span>
</div>
<div style="display:flex;gap:20px;flex-wrap:wrap">
<div style="flex:1;min-width:340px">
<div class="form-group">
<label>注入内容</label>
<textarea id="clip-text" class="form-control" rows="6"
placeholder="要注入到设备剪贴板的文字(支持中文/引号/换行)"></textarea>
</div>
<div class="toolbar" style="margin-bottom:8px">
<button class="btn btn-primary" onclick="injectClipboard()">注入剪贴板</button>
<span class="hint">注入后需在目标 App 的输入框长按选择"粘贴"</span>
</div>
<div class="help">实现:u2 setClipboard(jsonrpc 直连 atx-agent),实测支持中文、引号、换行;USB 设备首次连接会自动推送 atx-agent。</div>
</div>
<div style="flex:1;min-width:280px">
<div class="form-group">
<label>目标设备(本地 adb 含 USB + 设备池,可多选)</label>
<div class="device-list" id="clip-devices" style="max-height:300px">
<div class="empty" style="padding:20px">点击"刷新设备"加载</div>
</div>
</div>
</div>
</div>
<div id="clip-result-wrap" style="display:none;margin-top:16px">
<div class="section-title">注入结果</div>
<table class="table">
<thead><tr><th>设备</th><th>结果</th></tr></thead>
<tbody id="clip-result"></tbody>
</table>
</div>
</div>
<!-- 子分栏 2:adb 远程终端 -->
<div class="sub-panel" id="tools-sub-adb">
<div class="help">选中目标设备后,命令里的 <code>-s &lt;设备&gt;</code> 自动附加,直接写
<code>shell ls /sdcard</code> 即可。离线设备也可选,用"重连设备"恢复。
<b>禁止 kill-server / disconnect</b>(会中断共享的 adb transport,导致设备连接全部重建)。</div>
<div style="display:flex;gap:8px;margin:8px 0;align-items:center">
<label style="font-size:12px;color:var(--text-light)">目标设备</label>
<select id="adb-device" class="form-control" style="flex:1;max-width:340px;font-family:monospace"></select>
<button class="btn btn-xs" onclick="loadAdbDevices(true)" title="刷新设备列表">刷新</button>
</div>
<div style="display:flex;gap:8px;margin:8px 0">
<input type="text" id="adb-cmd-input" class="form-control" style="flex:1;font-family:monospace"
placeholder="shell ls /sdcard(选中设备后 -s 自动附加)"
onkeydown="if(event.key==='Enter')runAdbCmd()">
<button class="btn btn-primary" onclick="runAdbCmd()">执行</button>
</div>
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap">
<button class="btn btn-xs" onclick="fillAdbCmd('adb devices')">adb devices</button>
<button class="btn btn-xs" onclick="fillAdbCmd('shell dumpsys window | grep mCurrentFocus')">前台App</button>
<button class="btn btn-xs" onclick="fillAdbCmd('shell ls /sdcard')">列文件</button>
<button class="btn btn-xs" onclick="fillAdbCmd('shell screencap -p /sdcard/shot.png')">截屏</button>
<button class="btn btn-xs" onclick="fillAdbCmd('shell pm list packages -3')">已装应用</button>
<button class="btn btn-xs" onclick="fillAdbCmd('tcpip 5555')" title="USB 设备开启网络调试(重启设备后失效,需重新执行)">tcpip 5555 转网络调试</button>
</div>
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap">
<button class="btn btn-xs" onclick="reconnectAdbDevice()" title="重新 adb connect 该设备">重连设备</button>
</div>
<pre id="adb-output" class="adb-console">(执行命令后输出显示在这里)</pre>
</div>
<!-- 子分栏 3:Tailscale 管理 -->
<div class="sub-panel" id="tools-sub-ts">
<div class="help" id="ts-hint"></div>
<div class="toolbar" style="margin:8px 0">
<button class="btn btn-primary" onclick="loadTailscaleDevices()">刷新设备</button>
<button class="btn" onclick="openTailscaleAuthKey()">生成接入 Auth Key</button>
<input class="form-control list-search" id="search-tailscale" placeholder="搜索主机名/名称/IP...">
<span id="ts-status" class="text-muted" style="margin-left:10px"></span>
</div>
<table class="table">
<thead><tr>
<th data-sort="hostname" onclick="_lpSort('tailscale','hostname')">主机名</th><th data-sort="name" onclick="_lpSort('tailscale','name')">显示名</th><th data-sort="ip" onclick="_lpSort('tailscale','ip')">IP</th><th data-sort="os" onclick="_lpSort('tailscale','os')">系统</th><th data-sort="online" onclick="_lpSort('tailscale','online')">在线</th><th data-sort="authorized" onclick="_lpSort('tailscale','authorized')">授权</th><th data-sort="key_expiry" onclick="_lpSort('tailscale','key_expiry')">密钥过期</th><th>操作</th>
</tr></thead>
<tbody id="tb-tailscale"><tr><td colspan="8" class="empty">点击"刷新设备"加载</td></tr></tbody>
</table>
<div class="list-pager" id="pager-tailscale"></div>
<div class="help">"设置IP"可修改设备 IPv4(会断开当前连接,平台 serial 随之变化,需同步设备池);"密钥不过期"= 关闭设备密钥验证,设备不会被定期踢下线;auth key 生成后只显示一次,请立即复制。</div>
</div>
<!-- 子分栏 4:应用管理 -->
<div class="sub-panel" id="tools-sub-apks">
<div class="toolbar">
<label class="btn btn-primary" style="cursor:pointer;margin-bottom:0">
上传APK
<input type="file" accept=".apk" style="display:none" onchange="uploadApk(this)">
</label>
<button class="btn" onclick="loadApks()">刷新</button>
<input class="form-control list-search" id="search-apks" placeholder="搜索应用名/包名...">
<span class="hint" id="apk-hint"></span>
</div>
<table class="table">
<thead><tr>
<th data-sort="name" onclick="_lpSort('apks','name')">应用名</th><th data-sort="package_name" onclick="_lpSort('apks','package_name')">包名</th><th data-sort="version_name" onclick="_lpSort('apks','version_name')">版本</th><th data-sort="size" onclick="_lpSort('apks','size')">大小</th><th data-sort="upload_time" onclick="_lpSort('apks','upload_time')">上传时间</th><th>操作</th>
</tr></thead>
<tbody id="tb-apks"></tbody>
</table>
<div class="list-pager" id="pager-apks"></div>
</div>
<!-- 子分栏 5:应用版本管理 -->
<div class="sub-panel" id="tools-sub-appver">
<div class="toolbar">
<input type="text" id="appver-pkg" class="form-control" style="width:340px;font-family:monospace"
placeholder="包名,如 com.ss.android.ugc.aweme" onkeydown="if(event.key==='Enter')queryAppVers()">
<button class="btn btn-primary" onclick="queryAppVers()">查询版本</button>
<input class="form-control list-search" id="search-appver" placeholder="搜索设备/版本...">
<span id="appver-status" class="text-muted"></span>
</div>
<table class="table">
<thead><tr>
<th data-sort="serial" onclick="_lpSort('appver','serial')">设备</th>
<th data-sort="state" onclick="_lpSort('appver','state')">状态</th>
<th data-sort="version" onclick="_lpSort('appver','version')">版本号</th>
<th data-sort="code" onclick="_lpSort('appver','code')">versionCode</th>
</tr></thead>
<tbody id="tb-appver"><tr><td colspan="4" class="empty">输入包名后点击"查询版本"</td></tr></tbody>
</table>
<div class="list-pager" id="pager-appver"></div>
<div class="help">查询所有设备(本地 adb 含 USB + 设备池)上该包名的安装情况与版本号,用于对比各设备 App 版本是否一致;未连接的设备会自动连接(绝不断开,不影响任务)。</div>
</div>
<!-- 子分栏 6:设备已装应用 -->
<div class="sub-panel" id="tools-sub-devapps">
<div class="toolbar">
<select id="apps-device" class="form-control" style="width:280px;display:inline-block">
<option value="">选择设备...</option>
</select>
<button class="btn btn-primary" onclick="loadDeviceApps()">查看应用</button>
<input class="form-control list-search" id="search-devapps" placeholder="搜索包名/路径...">
<span class="hint" id="apps-hint"></span>
</div>
<div style="max-height:420px;overflow:auto;border:1px solid var(--border);border-radius:8px">
<table class="table" style="margin:0">
<thead><tr>
<th data-sort="label" onclick="_lpSort('devapps','label')">应用</th><th data-sort="version_name" onclick="_lpSort('devapps','version_name')">版本号</th><th data-sort="version_code" onclick="_lpSort('devapps','version_code')">versionCode</th><th>APK 路径</th>
</tr></thead>
<tbody id="tb-device-apps">
<tr><td colspan="4" class="empty">选择设备后点击"查看应用"</td></tr>
</tbody>
</table>
</div>
<div class="list-pager" id="pager-devapps"></div>
</div>
<!-- 子分栏 6:设备池管理(本地 SQLite 清单,任务调度的数据源) -->
<div class="sub-panel" id="tools-sub-devpool">
<div class="toolbar">
<input type="text" id="devpool-serial" class="form-control" style="width:220px;font-family:monospace"
placeholder="serial,如 100.100.10.20:5555" onkeydown="if(event.key==='Enter')addPoolDev()">
<input type="text" id="devpool-name" class="form-control" style="width:150px"
placeholder="备注名(可选)" onkeydown="if(event.key==='Enter')addPoolDev()">
<button class="btn btn-primary" onclick="addPoolDev()">添加设备</button>
<button class="btn" onclick="reconnectPoolDevices()" title="并发 adb connect 池内全部设备">一键重连</button>
<button class="btn" onclick="refreshPoolModels()" title="采集池内在线设备的型号(getprop)">采集型号</button>
<button class="btn" onclick="loadDevPool()">刷新</button>
<span id="devpool-status" class="text-muted"></span>
</div>
<table class="table table-hover table-sm" style="margin-top:8px">
<thead><tr>
<th>serial</th><th>名称</th><th>型号</th><th>在线</th><th>状态</th><th>添加时间</th><th>操作</th>
</tr></thead>
<tbody id="tb-devpool"></tbody>
</table>
<div class="help">
新增设备流程:手机开 USB 调试 → 连上后「adb 终端」执行 <code>tcpip 5555</code> 转网络调试 →
Tailscale 上线(同一账号)→ 在此添加 <code>IP:5555</code>(自动尝试连接)→
任务运行时 u2 自动推送 atx-agent。<b>停用 = 不参与任务调度</b>,删除 = 从池中移除(不影响其他系统)。
</div>
</div>
<!-- 子分栏 7:远程看屏(MJPEG 实时流 + 触控) -->
<div class="sub-panel" id="tools-sub-screen">
<div class="toolbar">
<select id="screen-device" class="form-control" style="width:300px;font-family:monospace"></select>
<button class="btn btn-primary" onclick="startScreenView()">开始看屏</button>
<button class="btn" onclick="stopScreenView()">停止</button>
<button class="btn" onclick="loadScreenDevices(true)">刷新设备</button>
<span id="screen-status" class="text-muted"></span>
</div>
<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap">
<div style="position:relative" id="screen-stage">
<img id="screen-img" alt="远程画面"
style="max-width:420px;border-radius:12px;border:1px solid var(--border);background:#000;cursor:crosshair;user-select:none">
<div id="screen-loading" class="empty" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.6);color:#fff;border-radius:12px">连接中(首次连接需推送 atx-agent,最长约 30 秒)...</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;min-width:200px">
<div style="font-size:12px;color:var(--text-light)">操作(鼠标拖拽=滑动,点击=轻触)</div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<button class="btn btn-xs" onclick="screenKey('back')">◀ 返回</button>
<button class="btn btn-xs" onclick="screenKey('home')">🏠 主页</button>
<button class="btn btn-xs" onclick="screenKey('recent')">📋 最近</button>
<button class="btn btn-xs" onclick="screenKey('menu')">☰ 菜单</button>
<button class="btn btn-xs" onclick="screenKey('power')">⏻ 电源</button>
<button class="btn btn-xs" onclick="screenKey('volume_up')">🔊 音量+</button>
<button class="btn btn-xs" onclick="screenKey('volume_down')">🔉 音量-</button>
</div>
<div style="display:flex;gap:6px;margin-top:4px">
<input type="text" id="screen-text" class="form-control" style="flex:1" placeholder="输入文字(需光标在输入框)"
onkeydown="if(event.key==='Enter')screenSendText()">
<button class="btn btn-xs" onclick="screenSendText()">发送</button>
</div>
<div class="help" style="margin-top:6px">画面为设备原生分辨率 JPEG 流(约 3-6 帧/秒)。设备息屏时画面为黑,可先点电源/亮屏。</div>
</div>
</div>
</div>
</div>
</div><!-- /.content -->
<!-- ========== 测试此步骤弹窗 ========== -->
<div id="test-overlay" class="modal-overlay" onclick="if(event.target===this)closeTestModal()">
<div class="modal" id="test-modal-box">
<div class="modal-header">
<h3 id="test-modal-title">测试步骤</h3>
<button class="close" onclick="closeTestModal()">&times;</button>
</div>
<div class="modal-body" id="test-modal-body"></div>
<div class="modal-footer"><button class="btn" onclick="closeTestModal()">关闭</button></div>
</div>
</div>
<!-- ========== 模态框 ========== -->
<div id="modal-overlay" class="modal-overlay" onclick="if(event.target===this)closeModal()">
<div class="modal" id="modal-box">
<div class="modal-header">
<h3 id="modal-title">标题</h3>
<button class="close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body" id="modal-body"></div>
<div class="modal-footer" id="modal-footer"></div>
</div>
</div>
<!-- ========== 自定义动作编辑模态框 ========== -->
<div id="action-modal-overlay" class="modal-overlay" onclick="if(event.target===this)closeActionModal()">
<div class="modal" id="action-modal-box" style="max-width:1000px">
<div class="modal-header">
<h3 id="action-modal-title">新建动作</h3>
<button class="close" onclick="closeActionModal()">&times;</button>
</div>
<div class="modal-body" id="action-modal-body" style="max-height:78vh;overflow:auto"></div>
<div class="modal-footer" id="action-modal-footer"></div>
</div>
</div>
<!-- ========== 元素抓取模态框(独立第二层,不覆盖任务编辑窗口)========== -->
<div id="el-picker-overlay" class="modal-overlay" style="z-index:1100" onclick="if(event.target===this)closeElPicker()">
<div class="modal" id="el-picker-box" style="max-width:900px">
<div class="modal-header">
<h3 id="el-picker-title">抓取元素</h3>
<button class="close" onclick="closeElPicker()">&times;</button>
</div>
<div class="modal-body" id="el-picker-body" style="max-height:75vh;padding:0;overflow:hidden;display:flex;flex-direction:column"></div>
<div class="modal-footer" id="el-picker-footer"></div>
</div>
</div>
<!-- ========== Toast ========== -->
<div id="toast" class="toast"></div>
<!-- 前端模块(按依赖顺序加载:base → list → 页面模块 → admin(含初始化)) -->
<script src="/static/admin/base.js"></script>
<script src="/static/admin/list.js"></script>
<script src="/static/admin/monitor.js"></script>
<script src="/static/admin/editor.js"></script>
<script src="/static/admin/tasks.js"></script>
<script src="/static/admin/tools.js"></script>
<script src="/static/admin/apps.js"></script>
<script src="/static/admin/admin.js"></script>
</body>
</html>