Files
auto_control/templates/admin/monitor.html
T

578 lines
34 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>
:root{
--primary:#3b82f6; --primary-dark:#2563eb; --success:#10b981; --danger:#ef4444;
--warning:#f59e0b; --bg:#f8fafc; --card:#ffffff; --border:#e2e8f0;
--text:#1e293b; --text-muted:#64748b; --text-light:#94a3b8;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);font-size:14px}
/* 顶部导航 */
.navbar{background:linear-gradient(135deg,#1e293b 0%,#334155 100%);padding:0 28px;display:flex;align-items:center;height:56px;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.navbar .brand{color:#fff;font-weight:700;font-size:16px;margin-right:32px;white-space:nowrap}
.navbar .tabs{display:flex;gap:4px;flex:1}
.navbar .tab{padding:8px 18px;color:#cbd5e1;font-size:14px;font-weight:500;cursor:pointer;border-radius:8px;transition:all .15s;border:none;background:none}
.navbar .tab:hover{color:#fff;background:rgba(255,255,255,.08)}
.navbar .tab.active{background:rgba(59,130,246,.25);color:#fff}
.navbar .user-info{color:#94a3b8;font-size:13px;display:flex;align-items:center;gap:12px}
.navbar .user-info a{color:#cbd5e1;text-decoration:none;font-size:13px}
.navbar .user-info a:hover{color:#fff}
/* 内容区 */
.content{max-width:1600px;margin:0 auto;padding:24px 28px}
.tab-panel{display:none}
.tab-panel.active{display:block}
.page-title{font-size:20px;font-weight:700;color:var(--text);margin:0 0 4px}
.page-sub{font-size:13px;color:var(--text-light);margin-bottom:20px}
/* 统计卡片 */
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:14px;margin-bottom:24px}
.stat-card{background:var(--card);border-radius:12px;padding:14px 16px;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid var(--border);position:relative;overflow:hidden}
.stat-card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--accent,#3b82f6)}
.stat-card .stat-num{font-size:24px;font-weight:700;line-height:1;margin-bottom:4px;color:var(--accent,#3b82f6)}
.stat-card .stat-lbl{font-size:12px;color:var(--text-muted);font-weight:500}
/* 工具栏 */
.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);padding:7px 16px;font-size:13px;transition:all .15s;cursor:pointer;background:var(--card);color:var(--text)}
.btn:hover{background:#f1f5f9;border-color:#cbd5e1}
.btn-primary{background:var(--primary);border-color:var(--primary);color:#fff}
.btn-primary:hover{background:var(--primary-dark)}
.btn-danger{background:var(--danger);border-color:var(--danger);color:#fff}
.btn-danger:hover{background:#dc2626}
.btn-success{background:var(--success);border-color:var(--success);color:#fff}
.btn-success:hover{background:#059669}
.btn-sm{padding:4px 12px;font-size:12px}
.btn-xs{padding:3px 10px;font-size:12px}
/* 通用列表分页栏 */
.list-pager{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:8px 0 20px}
.list-pager .lp-total{font-size:12px;color:var(--text-light);margin-right:auto}
.list-search{width:240px}
.adb-console{background:#0f172a;color:#e2e8f0;font-family:monospace;font-size:12px;padding:12px;border-radius:8px;min-height:200px;max-height:420px;overflow:auto;white-space:pre-wrap;word-break:break-all}
/* 区块标题 */
.section-title{font-size:15px;font-weight:600;color:var(--text);margin:0 0 12px;padding-bottom:8px;border-bottom:2px solid var(--primary);display:inline-block}
/* 表格 */
.table{background:var(--card);border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid var(--border);width:100%;border-collapse:collapse;margin-bottom:24px}
.table thead th{background:#f1f5f9;border-bottom:2px solid var(--border);font-weight:600;color:var(--text-muted);font-size:12px;letter-spacing:.3px;padding:12px 14px;text-align:left}
.table tbody td{padding:10px 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:hover td{background:#f8fafc}
/* 徽章 */
.label{padding:3px 10px;border-radius:6px;font-size:11px;font-weight:600;display:inline-block}
.label-success{background:#dcfce7;color:#166534}
.label-default{background:#f1f5f9;color:#64748b}
.label-warning{background:#fef3c7;color:#92400e}
.label-primary{background:#dbeafe;color:#1e40af}
.label-info{background:#e0e7ff;color:#3730a3}
.label-danger{background:#fee2e2;color:#991b1b}
/* 进度条 */
.progress{background:#e2e8f0;border-radius:4px;height:6px;overflow:hidden;display:inline-block;vertical-align:middle;width:70px}
.progress-bar{background:var(--primary);height:100%;border-radius:4px;transition:width .3s}
/* 圆点 */
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px}
.dot-on{background:var(--success);box-shadow:0 0 6px rgba(16,185,129,.5)}
.dot-off{background:var(--danger);opacity:.5}
/* 任务卡片 */
.jobs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px}
.job-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:14px;box-shadow:0 1px 3px rgba(0,0,0,.06);transition:all .15s}
.job-card:hover{box-shadow:0 4px 12px rgba(0,0,0,.08);transform:translateY(-2px)}
.job-card.running{border-color:var(--success);box-shadow:0 0 0 1px var(--success)}
.job-card .job-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px}
.job-card .job-name{font-size:14px;font-weight:600;color:var(--text)}
.job-card .job-type{font-size:11px;color:var(--text-light);background:#f1f5f9;padding:2px 8px;border-radius:4px;margin-top:2px;display:inline-block}
.job-card .job-body{font-size:12px;color:var(--text-muted);margin-bottom:10px;line-height:1.6}
.job-card .job-body .kv{display:inline-block;background:#f8fafc;border:1px solid var(--border);border-radius:4px;padding:1px 6px;margin:2px 2px 0 0}
.job-card .job-actions{display:flex;gap:6px;flex-wrap:wrap}
/* 空状态 */
.empty{text-align:center;padding:40px;color:var(--text-light)}
.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:.5}}
/* 模态框 */
.modal-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);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:0 20px 60px rgba(0,0,0,.2);width:100%;max-width:640px;margin-bottom:40px}
.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)}
.modal-header .close{background:none;border:none;font-size:22px;color:var(--text-light);cursor:pointer;padding:0;width:28px;height:28px;line-height:1}
.modal-header .close:hover{color:var(--text)}
.modal-body{padding:24px;max-height:60vh;overflow-y:auto}
.modal-footer{padding:16px 24px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px}
/* 表单 */
.form-group{margin-bottom:16px}
.form-group label{display:block;font-size:13px;font-weight:600;color:var(--text);margin-bottom:6px}
.form-group .help{font-size:12px;color:var(--text-light);margin-top:4px}
.form-control{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:13px;font-family:inherit;background:#fff;color:var(--text);transition:all .15s}
.form-control:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(59,130,246,.1)}
textarea.form-control{font-family:monospace;resize:vertical;min-height:100px}
select.form-control{cursor:pointer}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.form-check{display:flex;align-items:center;gap:8px}
.form-check input{width:16px;height:16px;cursor:pointer}
.form-check label{margin:0;cursor:pointer}
/* 设备多选列表 */
.device-list{max-height:200px;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:8px}
.device-list label{display:flex;align-items:center;gap:6px;padding:4px 8px;font-size:13px;font-weight:400;cursor:pointer;border-radius:4px}
.device-list label:hover{background:#f1f5f9}
.device-list input{width:14px;height:14px}
/* 日志 */
.log-toolbar{display:flex;gap:10px;margin-bottom:12px;align-items:center}
.log-content{background:#1e293b;color:#e2e8f0;border-radius:10px;padding:16px;font-family:"Cascadia Code",Consolas,monospace;font-size:12px;line-height:1.6;max-height:600px;overflow-y:auto;white-space:pre-wrap;word-break:break-all}
.log-content .log-err{color:#fca5a5}
.log-content .log-warn{color:#fcd34d}
.log-content .log-info{color:#93c5fd}
/* Toast */
.toast{position:fixed;top:20px;right:20px;z-index:2000;padding:12px 20px;border-radius:8px;font-size:14px;font-weight:500;box-shadow:0 4px 12px rgba(0,0,0,.15);display:none;max-width:400px}
.toast.show{display:block}
.toast-success{background:#dcfce7;color:#166534;border:1px solid #86efac}
.toast-error{background:#fee2e2;color:#991b1b;border:1px solid #fca5a5}
/* 截图弹窗 */
.shot-modal{max-width:none;width:min(440px,95vw)}
.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 8px rgba(0,0,0,.15);background:#1e293b;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}
.shot-toolbar .auto-label input{width:14px;height:14px}
/* 表头排序 */
.table thead th.sortable{cursor:pointer;user-select:none;white-space:nowrap}
.table thead th.sortable:hover{background:#e2e8f0}
.table thead th.sortable::after{content:" ⇅";color:#cbd5e1;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:#eff6ff}
.table .col-cb{width:36px;text-align:center}
.table .col-cb input{width:14px;height:14px;cursor:pointer;vertical-align:middle}
/* 步骤编辑器 */
.step-editor{display:none;border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-top:8px}
.step-editor.show{display:block}
.step-editor .se-head{background:#f1f5f9;padding:10px 14px;font-weight:600;font-size:13px;display:flex;justify-content:space-between;align-items:center}
.step-editor .se-body{display:grid;grid-template-columns:140px 1fr;min-height:280px}
.step-editor .se-lib{border-right:1px solid var(--border);padding:8px;background:#f8fafc}
.step-editor .se-lib .lib-title{font-size:11px;color:var(--text-light);font-weight:600;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
.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}
.step-editor .se-lib .lib-item:hover{background:#e2e8f0}
.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:4px;font-size:11px;flex-shrink:0}
.step-editor .se-canvas{padding:8px;overflow-y:auto;max-height:400px}
.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}
.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(59,130,246,.15)}
.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:5px;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:#f1f5f9;padding:1px 6px;border-radius:3px}
.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:3px;line-height:1}
.step-card .sc-actions button:hover{background:#f1f5f9;color:var(--text)}
.step-card .sc-body{display:none;padding:8px 10px;border-top:1px solid var(--border);background:#f8fafc}
.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 #8b5cf6}
.step-card.group-block .sc-ico{background:#8b5cf6}
.step-card.selected{border-color:var(--primary);box-shadow:0 0 0 2px rgba(59,130,246,.2)}
.step-children{margin-left:20px;margin-top:6px;border-left:2px dashed var(--border);padding-left:8px}
.step-children.drag-over{border-left-color:var(--primary);background:rgba(59,130,246,.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}
.se-lib .lib-section{margin-top:8px;padding-top:6px;border-top:1px solid var(--border)}
.se-lib .lib-item.custom .ico{background:#8b5cf6}
.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}
.se-lib .lib-item.custom .del-btn:hover{color:var(--danger)}
.se-toolbar{display:flex;gap:6px;padding:6px 8px;border-bottom:1px solid var(--border);background:#f8fafc}
.se-toolbar .btn{padding:4px 10px;font-size:12px}
/* 元素抓取弹窗(独立模态框,样式已内联在 HTML 中)*/
/* 设备选择页 */
.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}
.el-device-card:hover{border-color:var(--primary);box-shadow:0 2px 8px rgba(59,130,246,.12)}
.el-device-card .edc-icon{width:36px;height:36px;background:#f1f5f9;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)}
.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 8px;border-radius:4px;background:#dcfce7;color:#166534;flex-shrink:0}
/* 截图+元素树页 */
.el-picker-main{display:flex;flex:1;overflow:hidden}
.el-shot-wrap{flex:0 0 320px;border-right:1px solid var(--border);background:#1e293b;display:flex;flex-direction:column}
.el-shot-toolbar{padding:6px 10px;background:#334155;display:flex;gap:6px;align-items:center}
.el-shot-toolbar .btn{padding:3px 10px;font-size:12px;background:#475569;color:#fff;border:none}
.el-shot-toolbar .btn:hover{background:#64748b}
.el-shot-toolbar .el-shot-info{color:#cbd5e1;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:#94a3b8;font-size:13px;padding:40px}
.el-shot-overlay{position:absolute;border:1px solid rgba(59,130,246,.3);background:transparent;cursor:pointer;border-radius:2px;transition:all .1s}
.el-shot-overlay:hover{border-color:#3b82f6;background:rgba(59,130,246,.2)}
.el-shot-overlay.active{border:2px solid #ef4444;background:rgba(239,68,68,.2)}
/* 右侧元素列表 */
.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}
.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:#eff6ff}
.el-picker-item.active{background:#dbeafe}
.el-picker-item:last-child{border-bottom:none}
.el-picker-item .ep-depth{color:var(--text-light);font-family:monospace;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:var(--primary);background:#dbeafe;padding:1px 6px;border-radius:3px;flex-shrink:0}
</style>
</head>
<body>
<!-- 顶部导航 -->
<nav class="navbar">
<div class="brand">设备自动化后台</div>
<div class="tabs">
<button class="tab active" onclick="showTab('monitor')">监控</button>
<button class="tab" onclick="showTab('tasks')">任务</button>
<button class="tab" onclick="showTab('apps')" data-perm="apks">应用</button>
<button class="tab" onclick="showTab('groups')">分组</button>
<button class="tab" onclick="showTab('logs')" data-perm="logs">日志</button>
<button class="tab" onclick="showTab('users')" data-perm="admin">用户</button>
<button class="tab" onclick="showTab('maintenance')" data-perm="admin">维护</button>
</div>
<div class="user-info">
<span id="me-username">...</span>
<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" onclick="apiAction('/api/release')" data-perm="devices">强制释放占用</button>
<button class="btn btn-primary" id="btn-scan-fg" onclick="scanForeground()" data-perm="devices">扫描前台App</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 class="sortable" data-key="serial" onclick="sortBy('serial')">设备</th>
<th class="sortable" data-key="model" onclick="sortBy('model')">型号</th>
<th class="sortable" data-key="stf_occupied" onclick="sortBy('stf_occupied')">STF</th>
<th class="sortable" data-key="worker_status" onclick="sortBy('worker_status')">任务状态</th>
<th class="sortable" data-key="foreground_app" onclick="sortBy('foreground_app')">前台App</th>
<th class="sortable" data-key="progress" onclick="sortBy('progress')">进度</th>
<th class="sortable" data-key="task_job" onclick="sortBy('task_job')">当前任务</th>
<th class="sortable" data-key="current_action" onclick="sortBy('current_action')">当前动作</th>
<th class="sortable" data-key="last_error" onclick="sortBy('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="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>ID</th><th>任务名</th><th>类型</th><th>启用</th><th>目标</th><th>调度</th><th>下次运行</th><th>重试</th><th>操作</th>
</tr></thead>
<tbody id="tb-tasks"></tbody>
</table>
<div class="list-pager" id="pager-tasks"></div>
<!-- 自定义动作管理 -->
<div style="margin-top:28px">
<div class="page-title" style="font-size:16px">自定义动作管理</div>
<div class="page-sub">把常用步骤打包成可复用的动作,拖入任务步骤编辑器使用;也可在任务编辑器里"☑选中步骤→打包"生成</div>
<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>
<table class="table">
<thead><tr><th>图标</th><th>动作名</th><th>步骤数</th><th>创建时间</th><th>操作</th></tr></thead>
<tbody id="tb-actions"></tbody>
</table>
<div class="list-pager" id="pager-actions"></div>
</div>
</div>
<!-- ========== Tab: 应用 ========== -->
<div id="tab-apps" class="tab-panel">
<div class="page-title">应用管理</div>
<div class="page-sub">上传 APK 文件,批量安装到设备(运行中设备自动跳过)</div>
<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>应用名</th><th>包名</th><th>版本</th><th>大小</th><th>上传时间</th><th>操作</th>
</tr></thead>
<tbody id="tb-apks"></tbody>
</table>
<div class="list-pager" id="pager-apks"></div>
<!-- 设备已装应用 -->
<div style="margin-top:28px">
<div class="page-title" style="font-size:16px">设备已装应用</div>
<div class="page-sub">查看某台设备上安装的所有应用(含版本号)</div>
<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>应用</th><th>版本号</th><th>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>
</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>分组名</th><th>描述</th><th>设备数</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>ID</th><th>用户名</th><th>管理员</th><th>权限</th><th>操作</th>
</tr></thead>
<tbody id="tb-users"></tbody>
</table>
<div class="list-pager" id="pager-users"></div>
</div>
<!-- ========== Tab 7: 维护(仅管理员) ========== -->
<div id="tab-maintenance" class="tab-panel">
<div class="page-title">维护</div>
<div class="page-sub">管理员专用:STF 服务管理与 adb 远程终端</div>
<div class="section-title">STF 服务</div>
<div class="toolbar">
<button class="btn btn-danger" onclick="restartStf()">一键重启 STF 容器</button>
<span id="stf-restart-status" class="text-muted" style="margin-left:10px"></span>
</div>
<div class="help" style="margin-top:8px">
通过 SSH 到部署机执行 <code>docker restart</code>(目标与容器名可用环境变量
<code>STF_SSH_TARGET</code> / <code>STF_DOCKER_CONTAINER</code> 配置)。
影响:STF 约 30 秒不可用,期间设备占用状态重置;<b>有任务在运行时会被拒绝执行</b>。
</div>
<div class="section-title" style="margin-top:16px">adb 远程终端</div>
<div class="help">选中目标设备后,命令里的 <code>-s &lt;设备&gt;</code> 自动附加,直接写
<code>shell ls /sdcard</code> 即可。离线设备也可选,用"重连设备"恢复。
<b>禁止 kill-server / disconnect</b>(会中断 STF provider 的设备监控)。</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()" 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>
</div>
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap">
<button class="btn btn-xs btn-danger" onclick="uninstallStfAgent()" title="卸载 jp.co.cyberagent.stf,STF 会在设备重连时重新推送">卸载 STF agent(卡死时用)</button>
<button class="btn btn-xs" onclick="reconnectAdbDevice()" title="重新 adb connect 该设备">重连设备</button>
<button class="btn btn-xs" onclick="checkStfAgent()" title="查看 jp.co.cyberagent.stf 是否已安装">检查 agent</button>
</div>
<pre id="adb-output" class="adb-console">(执行命令后输出显示在这里)</pre>
</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>
<script src="/static/admin/monitor.js"></script>
</body>
</html>