1470 lines
48 KiB
PHP
1470 lines
48 KiB
PHP
<?php
|
||
session_start();
|
||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||
header("Pragma: no-cache");
|
||
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
|
||
|
||
require_once 'auth.php';
|
||
|
||
$expire = 1800;
|
||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
|
||
session_destroy();
|
||
header("Location: login.php");
|
||
exit;
|
||
}
|
||
$_SESSION['login_time'] = time();
|
||
$userName = htmlspecialchars($_SESSION['username']);
|
||
$userRole = $_SESSION['role'];
|
||
$roleText = $userRole === 'admin' ? '管理员' : '超级管理员';
|
||
|
||
$dbHost = "10.150.117.190";
|
||
$dbUser = "root";
|
||
$dbPwd = "hp93000";
|
||
$dbName = "alert_mail_stat";
|
||
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
|
||
mysqli_set_charset($connConfig, "utf8mb4");
|
||
$configSql = "SELECT k,v FROM sys_config";
|
||
$configRes = mysqli_query($connConfig, $configSql);
|
||
$sysConfig = [];
|
||
while ($row = mysqli_fetch_assoc($configRes)) {
|
||
$sysConfig[$row['k']] = $row['v'];
|
||
}
|
||
mysqli_close($connConfig);
|
||
|
||
$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统");
|
||
$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff");
|
||
$pageSize = $sysConfig['page_size'] ?? "20";
|
||
$footerText = htmlspecialchars($sysConfig['footer_text'] ?? "");
|
||
$sidebarRawLinks = $sysConfig['sidebar_links'] ?? "";
|
||
$currentLang = $sysConfig['lang'] ?? "zh";
|
||
|
||
$langMap = [
|
||
'zh' => [
|
||
'html_lang' => 'zh-CN',
|
||
'page_subtitle' => '爱德万测试 · 运维告警监控平台',
|
||
'monitor_panel' => '监控面板',
|
||
'alert_overview' => '告警监控总览',
|
||
'alert_list_page' => '历史告警查询',
|
||
'work_order_mgr' => '工单系统',
|
||
'docker_mgr' => 'Docker容器管理',
|
||
'system_manager' => '角色管理',
|
||
'quick_link' => '快捷外链',
|
||
'system_manage' => '系统管理',
|
||
'system_config' => '后台配置管理',
|
||
'current_user' => '当前用户:',
|
||
'sys_time' => '系统时间:',
|
||
'refresh_all' => '刷新全部数据',
|
||
'logout' => '退出登录',
|
||
'lang_text' => '语言:',
|
||
'container_list' => '容器列表',
|
||
'image_list' => '镜像列表',
|
||
'container_log' => '容器日志',
|
||
'network_info' => '网络详情',
|
||
'exec_container' => '进入容器执行命令',
|
||
'restart' => '重启',
|
||
'stop' => '停止',
|
||
'remove' => '强制删除',
|
||
'remove_image' => '删除镜像',
|
||
'bind_repo' => '绑定镜像仓库',
|
||
'input_cmd' => '仅允许 ls/pwd/cat/ps/df/free/top/du/whoami/env,禁止rm/wget/curl/chmod等高危指令',
|
||
'execute' => '执行',
|
||
'close' => '关闭',
|
||
'loading' => '加载中...',
|
||
'no_container' => '暂无容器',
|
||
'no_image' => '无本地镜像',
|
||
'request_fail' => '接口请求失败',
|
||
'container_id' => '容器ID',
|
||
'container_name' => '容器名称',
|
||
'image_name' => '镜像',
|
||
'status' => '运行状态',
|
||
'port_mapping' => '端口映射',
|
||
'operate' => '操作',
|
||
'repo' => '镜像仓库',
|
||
'tag' => '镜像标签',
|
||
'image_id' => '镜像ID',
|
||
'size' => '镜像大小',
|
||
'create_time' => '创建时间',
|
||
'confirm_remove' => '确认强制删除容器?操作不可恢复!',
|
||
'confirm_remove_img' => '确认删除本地镜像?不可恢复!',
|
||
'op_success' => '操作成功,已刷新状态',
|
||
'op_failed' => '操作执行失败',
|
||
'loading_mask' => '数据加载中,请稍候...',
|
||
'create_container' => '新建容器',
|
||
'batch_restart' => '批量重启',
|
||
'batch_stop' => '批量停止',
|
||
'batch_remove' => '批量删除',
|
||
'search_placeholder' => '搜索容器名称/镜像',
|
||
'op_menu' => '操作 ▼',
|
||
'view_log' => '查看日志',
|
||
'view_network' => '查看网络',
|
||
'enter_exec' => '交互式终端',
|
||
'stat_total' => '容器总数',
|
||
'stat_running' => '运行中',
|
||
'stat_stopped' => '已停止',
|
||
'stat_image' => '本地镜像',
|
||
'tip_exec_once' => '单次命令执行',
|
||
'tip_websocket' => '实时交互式TTY终端,支持输入、复制、窗口自适应',
|
||
'repo_addr' => '仓库地址',
|
||
'repo_user' => '仓库账号',
|
||
'repo_pwd' => '仓库密码',
|
||
'repo_name' => '仓库名称',
|
||
'bind_repo_title' => '绑定私有镜像仓库'
|
||
],
|
||
'en' => [
|
||
'html_lang' => 'en',
|
||
'page_subtitle' => 'Advantest · O&M Alert Monitor Platform',
|
||
'monitor_panel' => 'Monitor Panel',
|
||
'alert_overview' => 'Alert Overview',
|
||
'alert_list_page' => 'Alert Pagination List',
|
||
'work_order_mgr' => 'Work Order System',
|
||
'docker_mgr' => 'Docker Manager',
|
||
'system_manager' => 'Role & Permission Manage',
|
||
'quick_link' => 'Quick Links',
|
||
'system_manage' => 'System Manage',
|
||
'system_config' => 'System Config',
|
||
'current_user' => 'User: ',
|
||
'sys_time' => 'System Time: ',
|
||
'refresh_all' => 'Refresh All Data',
|
||
'logout' => 'Logout',
|
||
'lang_text' => 'Lang: ',
|
||
'container_list' => 'Container List',
|
||
'image_list' => 'Image List',
|
||
'container_log' => 'Container Logs',
|
||
'network_info' => 'Network Info',
|
||
'exec_container' => 'Exec Container Command',
|
||
'restart' => 'Restart',
|
||
'stop' => 'Stop',
|
||
'remove' => 'Force Remove',
|
||
'remove_image' => 'Remove Image',
|
||
'bind_repo' => 'Bind Registry',
|
||
'input_cmd' => 'Only allow ls/pwd/cat/ps/df/free/top/du/whoami/env, ban rm/wget/curl/chmod',
|
||
'execute' => 'Run',
|
||
'close' => 'Close',
|
||
'loading' => 'Loading...',
|
||
'no_container' => 'No Containers',
|
||
'no_image' => 'No Local Images',
|
||
'request_fail' => 'API Request Failed',
|
||
'container_id' => 'Container ID',
|
||
'container_name' => 'Name',
|
||
'image_name' => 'Image',
|
||
'status' => 'Status',
|
||
'port_mapping' => 'Ports',
|
||
'operate' => 'Operate',
|
||
'repo' => 'Repository',
|
||
'tag' => 'Tag',
|
||
'image_id' => 'Image ID',
|
||
'size' => 'Size',
|
||
'create_time' => 'Created At',
|
||
'confirm_remove' => 'Confirm force delete container? Irreversible!',
|
||
'confirm_remove_img' => 'Confirm delete local image? Irreversible!',
|
||
'op_success' => 'Operation succeeded, refreshed status',
|
||
'op_failed' => 'Operation failed',
|
||
'loading_mask' => 'Loading data, please wait...',
|
||
'create_container' => 'Create Container',
|
||
'batch_restart' => 'Batch Restart',
|
||
'batch_stop' => 'Batch Stop',
|
||
'batch_remove' => 'Batch Remove',
|
||
'search_placeholder' => 'Search container / image',
|
||
'op_menu' => 'Operate ▼',
|
||
'view_log' => 'Logs',
|
||
'view_network' => 'Network',
|
||
'enter_exec' => 'Interactive Terminal',
|
||
'stat_total' => 'Total Containers',
|
||
'stat_running' => 'Running',
|
||
'stat_stopped' => 'Stopped',
|
||
'stat_image' => 'Local Images',
|
||
'tip_exec_once' => 'Single command execution',
|
||
'tip_websocket' => 'Real-time interactive TTY terminal, input & resize support',
|
||
'repo_addr' => 'Registry Address',
|
||
'repo_user' => 'Username',
|
||
'repo_pwd' => 'Password',
|
||
'repo_name' => 'Registry Name',
|
||
'bind_repo_title' => 'Bind Private Registry'
|
||
]
|
||
];
|
||
$t = $langMap[$currentLang];
|
||
|
||
$sidebarLinkList = [];
|
||
if (!empty($sidebarRawLinks)) {
|
||
$lines = explode("\n", $sidebarRawLinks);
|
||
foreach ($lines as $line) {
|
||
$line = trim($line);
|
||
if (empty($line)) continue;
|
||
$item = explode("|", $line, 2);
|
||
if (count($item) === 2) {
|
||
$sidebarLinkList[] = [
|
||
"name" => trim($item[0]),
|
||
"url" => trim($item[1])
|
||
];
|
||
}
|
||
}
|
||
}
|
||
$pageSid = session_id();
|
||
|
||
// WebSocket HMAC 签名参数(和docker_api.py密钥完全一致)
|
||
$WS_SECRET = "DockerAdminPlatform2026SecretKey123456789";
|
||
$wsUser = $_SESSION['user_id'];
|
||
$wsRole = $_SESSION['role'];
|
||
$wsTs = time();
|
||
$wsRawSign = $wsUser . "|" . $wsRole . "|" . $wsTs;
|
||
$wsSign = hash_hmac("sha256", $wsRawSign, $WS_SECRET);
|
||
$wsClientIp = $_SERVER['REMOTE_ADDR'];
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="<?php echo $t['html_lang']; ?>">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?php echo $pageTitle; ?> - Advantest <?php echo $t['page_subtitle']; ?></title>
|
||
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
|
||
<link rel="stylesheet" href="/static/css/xterm.css">
|
||
<script src="/static/js/jquery.min.js"></script>
|
||
<script src="/static/js/socket.io.min.js"></script>
|
||
<script src="/static/js/xterm.min.js"></script>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{
|
||
background:#f9fafc;
|
||
font-family:"Inter","PingFang SC","Microsoft YaHei";
|
||
color:#1d2939;
|
||
line-height:1.6;
|
||
display:flex;
|
||
flex-direction:column;
|
||
height:100vh;
|
||
overflow:hidden;
|
||
}
|
||
.wrap-main{
|
||
display:flex;
|
||
flex:1;
|
||
overflow:hidden;
|
||
}
|
||
.sidebar{
|
||
width:240px;
|
||
background:#1e293b;
|
||
color:#e2e8f0;
|
||
flex-shrink:0;
|
||
overflow-y:auto;
|
||
}
|
||
.sidebar-brand{
|
||
padding:26px 24px;
|
||
border-bottom:1px solid rgba(255,255,255,0.1);
|
||
}
|
||
.sidebar-brand-en{
|
||
font-size:20px;
|
||
font-weight:700;
|
||
letter-spacing:1px;
|
||
color:#fff;
|
||
}
|
||
.sidebar-brand-cn{
|
||
font-size:13px;
|
||
color:#a5b4fc;
|
||
margin-top:4px;
|
||
}
|
||
.sidebar-menu{
|
||
padding:16px 0;
|
||
}
|
||
.menu-title{
|
||
padding:10px 24px;
|
||
font-size:12px;
|
||
color:#94a3b8;
|
||
letter-spacing:1px;
|
||
}
|
||
.menu-item{
|
||
display:flex;
|
||
align-items:center;
|
||
gap:12px;
|
||
padding:14px 24px;
|
||
cursor:pointer;
|
||
font-size:14px;
|
||
transition:0.2s;
|
||
color:#cbd5e1;
|
||
}
|
||
.menu-item i{
|
||
font-size:16px;
|
||
width:20px;
|
||
text-align:center;
|
||
}
|
||
.menu-item.active{
|
||
background:rgba(255,255,255,0.12);
|
||
color:#fff;
|
||
border-left:4px solid #3b82f6;
|
||
}
|
||
.menu-item:hover:not(.active){
|
||
background:rgba(255,255,255,0.06);
|
||
color:#fff;
|
||
}
|
||
.main-content{
|
||
flex:1;
|
||
overflow-y:auto;
|
||
display:flex;
|
||
flex-direction:column;
|
||
}
|
||
.header{
|
||
height:64px;
|
||
padding:0 40px;
|
||
background:#ffffff;
|
||
border-bottom:1px solid #eef2fb;
|
||
color:#152c5b;
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
box-shadow:0 2px 12px rgba(21,44,91,0.05);
|
||
flex-shrink:0;
|
||
}
|
||
.header-left{
|
||
display:flex;
|
||
align-items:center;
|
||
gap:14px;
|
||
}
|
||
.header-brand-en{
|
||
font-size:24px;
|
||
font-weight:700;
|
||
letter-spacing:2px;
|
||
color:#152c5b;
|
||
}
|
||
.header-brand-cn{
|
||
font-size:15px;
|
||
color:#64748b;
|
||
}
|
||
.header-right{
|
||
display:flex;
|
||
align-items:center;
|
||
gap:18px;
|
||
font-size:14px;
|
||
flex-wrap:wrap;
|
||
}
|
||
.user-info,.time-info,.lang-box{
|
||
color:#475569;
|
||
display:flex;
|
||
align-items:center;
|
||
gap:6px;
|
||
}
|
||
.lang-box select{
|
||
padding:4px 8px;
|
||
border:1px solid #cbd5e1;
|
||
border-radius:6px;
|
||
}
|
||
.btn-base{
|
||
border:none;
|
||
padding:9px 20px;
|
||
border-radius:8px;
|
||
cursor:pointer;
|
||
font-size:14px;
|
||
display:flex;
|
||
align-items:center;
|
||
gap:6px;
|
||
transition:all 0.2s ease;
|
||
}
|
||
.btn-outline{
|
||
background:#ffffff;
|
||
color:#152c5b;
|
||
border:1px solid #cbd5e1;
|
||
}
|
||
.btn-outline:hover{
|
||
background:#f7f8fc;
|
||
border-color:#152c5b;
|
||
}
|
||
.btn-primary{
|
||
background:#152c5b;
|
||
color:#fff;
|
||
}
|
||
.btn-primary:hover{
|
||
background:#0f1e42;
|
||
}
|
||
.btn-danger{
|
||
background:#dc2626;
|
||
color:#fff;
|
||
}
|
||
.btn-danger:hover{
|
||
background:#b91c1c;
|
||
}
|
||
.btn-base:disabled{
|
||
opacity:0.6;
|
||
cursor:not-allowed;
|
||
}
|
||
.container{
|
||
padding:36px 40px;
|
||
max-width:100%;
|
||
flex:1;
|
||
}
|
||
.base-card{
|
||
background:#fff;
|
||
border-radius:16px;
|
||
padding:32px;
|
||
box-shadow:0 3px 18px rgba(21,44,91,0.06);
|
||
border:1px solid #eef2fb;
|
||
margin-bottom:36px;
|
||
}
|
||
.title-wrap{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
margin-bottom:26px;
|
||
padding-bottom:16px;
|
||
border-bottom:1px solid #eef2fb;
|
||
}
|
||
.title-left{
|
||
display:flex;
|
||
align-items:center;
|
||
gap:12px;
|
||
font-size:19px;
|
||
font-weight:600;
|
||
color:#152c5b;
|
||
}
|
||
.title-left i{color:#2563eb;font-size:21px}
|
||
.tab-bar{
|
||
display:flex;
|
||
border-bottom:1px solid #eef2fb;
|
||
margin-bottom:24px;
|
||
}
|
||
.tab-item{
|
||
padding:14px 24px;
|
||
cursor:pointer;
|
||
font-size:15px;
|
||
color:#64748b;
|
||
}
|
||
.tab-item.active{
|
||
border-bottom:2px solid #3b82f6;
|
||
color:#152c5b;
|
||
font-weight:500;
|
||
}
|
||
.tab-panel{display:none;}
|
||
.tab-panel.active{display:block;}
|
||
.stat-row{
|
||
display:grid;
|
||
grid-template-columns: repeat(4,1fr);
|
||
gap:20px;
|
||
margin-bottom:24px;
|
||
}
|
||
.stat-card{
|
||
background:#fff;
|
||
padding:20px;
|
||
border-radius:12px;
|
||
box-shadow:0 2px 12px rgba(21,44,91,0.05);
|
||
border:1px solid #eef2fb;
|
||
}
|
||
.stat-label{
|
||
font-size:14px;
|
||
color:#64748b;
|
||
margin-bottom:8px;
|
||
}
|
||
.stat-num{
|
||
font-size:26px;
|
||
font-weight:700;
|
||
color:#152c5b;
|
||
}
|
||
.tool-bar{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
margin-bottom:16px;
|
||
}
|
||
.search-input{
|
||
width:320px;
|
||
height:40px;
|
||
padding:0 14px;
|
||
border:1px solid #cbd5e1;
|
||
border-radius:8px;
|
||
font-size:14px;
|
||
}
|
||
.batch-group{
|
||
display:flex;
|
||
gap:10px;
|
||
}
|
||
.table-wrap{overflow-x:auto}
|
||
table{width:100%;border-collapse:collapse;font-size:14px}
|
||
th{
|
||
background:#f9fafc;
|
||
color:#475569;
|
||
font-weight:600;
|
||
padding:16px 18px;
|
||
text-align:left;
|
||
border-bottom:2px solid #e2e8f0;
|
||
white-space:nowrap;
|
||
}
|
||
td{
|
||
padding:18px;
|
||
border-bottom:1px solid #f1f5f9;
|
||
color:#334155;
|
||
}
|
||
tbody tr{transition:background 0.2s}
|
||
tbody tr:hover{background:#f7f8fc}
|
||
tbody tr:nth-child(even){background:#fbfcfe}
|
||
.container-name{font-weight:600;color:#152c5b;}
|
||
.tag-success{
|
||
background:#dcfce7;
|
||
color:#15803d;
|
||
padding:4px 10px;
|
||
border-radius:6px;
|
||
font-size:13px;
|
||
}
|
||
.tag-danger{
|
||
background:#fee2e2;
|
||
color:#b91c1c;
|
||
padding:4px 10px;
|
||
border-radius:6px;
|
||
font-size:13px;
|
||
}
|
||
.tag-gray{
|
||
background:#f3f4f6;
|
||
color:#4b5563;
|
||
padding:4px 10px;
|
||
border-radius:6px;
|
||
font-size:13px;
|
||
}
|
||
.op-dropdown{
|
||
position:relative;
|
||
display:inline-block;
|
||
}
|
||
.op-dropdown-menu{
|
||
position:absolute;
|
||
top:100%;
|
||
left:0;
|
||
background:#fff;
|
||
border:1px solid #eef2fb;
|
||
border-radius:8px;
|
||
box-shadow:0 4px 16px rgba(21,44,91,0.1);
|
||
z-index:99;
|
||
min-width:130px;
|
||
display:none;
|
||
}
|
||
.op-dropdown-item{
|
||
padding:10px 16px;
|
||
cursor:pointer;
|
||
font-size:14px;
|
||
}
|
||
.op-dropdown-item:hover{background:#f7f8fc;}
|
||
.empty-state{
|
||
text-align:center;
|
||
padding:80px 20px;
|
||
color:#94a3b8;
|
||
font-size:15px;
|
||
}
|
||
.empty-icon{
|
||
font-size:56px;
|
||
margin-bottom:18px;
|
||
opacity:0.35;
|
||
}
|
||
.modal-mask{
|
||
position:fixed;
|
||
left:0;top:0;
|
||
width:100%;height:100%;
|
||
background:rgba(0,0,0,0.55);
|
||
display:none;
|
||
z-index:999;
|
||
align-items:center;
|
||
justify-content:center;
|
||
}
|
||
.modal-box{
|
||
width:88%;
|
||
max-width:1200px;
|
||
min-width:720px;
|
||
background:#fff;
|
||
border-radius:16px;
|
||
overflow:hidden;
|
||
border:1px solid #eef2fb;
|
||
box-shadow:0 10px 40px rgba(21,44,91,0.15);
|
||
display:flex;
|
||
flex-direction:column;
|
||
}
|
||
.modal-head{
|
||
height:56px;
|
||
line-height:56px;
|
||
padding:0 24px;
|
||
border-bottom:1px solid #eef2fb;
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
font-size:16px;
|
||
font-weight:600;
|
||
color:#152c5b;
|
||
flex-shrink:0;
|
||
}
|
||
.modal-close{
|
||
cursor:pointer;
|
||
font-size:20px;
|
||
color:#94a3b8;
|
||
}
|
||
.modal-body{
|
||
padding:0;
|
||
height:62vh;
|
||
overflow:auto;
|
||
background:#111827;
|
||
color:#e5e7eb;
|
||
white-space:pre-wrap;
|
||
word-break:break-all;
|
||
font-family:Consolas,Monaco,monospace;
|
||
font-size:13px;
|
||
}
|
||
.modal-footer{
|
||
padding:16px 24px;
|
||
border-top:1px solid #eef2fb;
|
||
display:flex;
|
||
gap:12px;
|
||
align-items:center;
|
||
flex-shrink:0;
|
||
}
|
||
.cmd-input{
|
||
flex:1;
|
||
height:40px;
|
||
padding:0 14px;
|
||
border:1px solid #cbd5e1;
|
||
border-radius:8px;
|
||
font-size:14px;
|
||
}
|
||
.global-loading{
|
||
position:fixed;
|
||
inset:0;
|
||
background:rgba(255,255,255,0.7);
|
||
z-index:9999;
|
||
display:none;
|
||
align-items:center;
|
||
justify-content:center;
|
||
flex-direction:column;
|
||
gap:16px;
|
||
}
|
||
.global-loading i{
|
||
font-size:42px;
|
||
color:#2563eb;
|
||
}
|
||
.sid-tip{
|
||
padding:10px 40px;
|
||
color:#64748b;
|
||
font-size:13px;
|
||
}
|
||
#createModal .modal-body{
|
||
background:#fff;
|
||
color:#1d2939;
|
||
height:auto;
|
||
max-height:70vh;
|
||
padding:24px;
|
||
}
|
||
#repoModal .modal-body{
|
||
background:#fff;
|
||
color:#1d2939;
|
||
height:auto;
|
||
max-height:70vh;
|
||
padding:24px;
|
||
}
|
||
.form-item{
|
||
margin-bottom:16px;
|
||
}
|
||
.form-item label{
|
||
display:block;
|
||
margin-bottom:6px;
|
||
font-weight:500;
|
||
}
|
||
.form-input{
|
||
width:100%;
|
||
height:40px;
|
||
padding:0 14px;
|
||
border:1px solid #cbd5e1;
|
||
border-radius:8px;
|
||
font-size:14px;
|
||
}
|
||
@media (max-width:992px){
|
||
.wrap-main{flex-direction:column;}
|
||
.sidebar{width:100%;height:auto}
|
||
.header{padding:0 24px;flex-direction:column;gap:16px;height:auto}
|
||
.container{padding:24px}
|
||
.base-card{padding:24px}
|
||
.modal-box{width:96%;min-width:auto;}
|
||
.stat-row{grid-template-columns: repeat(2,1fr);}
|
||
.tool-bar{flex-direction:column;gap:12px;align-items:flex-start;}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="global-loading" id="globalLoading">
|
||
<i class="fa fa-spinner fa-spin"></i>
|
||
<span><?php echo $t['loading_mask']; ?></span>
|
||
</div>
|
||
|
||
<div class="wrap-main">
|
||
<div class="sidebar">
|
||
<div class="sidebar-brand">
|
||
<div class="sidebar-brand-en">Advantest</div>
|
||
<div class="sidebar-brand-cn"><?php echo $t['page_subtitle']; ?></div>
|
||
</div>
|
||
<div class="sidebar-menu">
|
||
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div>
|
||
<div class="menu-item" onclick="location.href='dashboard.php'">
|
||
<i class="fa fa-line-chart"></i>
|
||
<span><?php echo $t['alert_overview']; ?></span>
|
||
</div>
|
||
<div class="menu-item" onclick="location.href='alert_list.php'">
|
||
<i class="fa fa-list"></i>
|
||
<span><?php echo $t['alert_list_page']; ?></span>
|
||
</div>
|
||
<div class="menu-item" onclick="location.href='work_order.php'">
|
||
<i class="fa fa-ticket"></i>
|
||
<span><?php echo $t['work_order_mgr']; ?></span>
|
||
</div>
|
||
<div class="menu-item active" onclick="location.href='docker_mgr.php'">
|
||
<i class="fa fa-cubes"></i>
|
||
<span><?php echo $t['docker_mgr']; ?></span>
|
||
</div>
|
||
<?php if (!empty($sidebarLinkList)): ?>
|
||
<div class="menu-title"><?php echo $t['quick_link']; ?></div>
|
||
<?php foreach ($sidebarLinkList as $link): ?>
|
||
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>')">
|
||
<i class="fa fa-external-link"></i>
|
||
<span><?php echo htmlspecialchars($link['name']); ?></span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
<div class="menu-title"><?php echo $t['system_manage']; ?></div>
|
||
<div class="menu-item" onclick="location.href='system_manage.php'">
|
||
<i class="fa fa-users"></i>
|
||
<span><?php echo $t['system_manager']; ?></span>
|
||
</div>
|
||
<div class="menu-item" onclick="location.href='admin.php'">
|
||
<i class="fa fa-cog"></i>
|
||
<span><?php echo $t['system_config']; ?></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<div>
|
||
<div class="header-brand-en">Advantest</div>
|
||
<div class="header-brand-cn"><?php echo $t['page_subtitle']; ?></div>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||
<span class="time-info"><i class="fa fa-clock-o"></i><?php echo $t['sys_time']; ?><span id="updateTime">--</span></span>
|
||
<div class="lang-box">
|
||
<span><?php echo $t['lang_text']; ?></span>
|
||
<select id="langSwitch">
|
||
<option value="zh" <?php echo $currentLang==='zh'?'selected':''; ?>>中文</option>
|
||
<option value="en" <?php echo $currentLang==='en'?'selected':''; ?>>EN</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn-base btn-outline" id="refreshAll"><i class="fa fa-refresh"></i><?php echo $t['refresh_all']; ?></button>
|
||
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i><?php echo $t['logout']; ?></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sid-tip">
|
||
当前页面SID:<?php echo $pageSid; ?> | 接口SID:<?php echo $pageSid; ?>(SID一致,会话互通)
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="base-card">
|
||
<div class="title-wrap">
|
||
<div class="title-left"><i class="fa fa-cubes"></i><?php echo $t['docker_mgr']; ?></div>
|
||
<div style="display:flex;gap:12px;">
|
||
<button class="btn-base btn-outline" onclick="openRepoModal()"><i class="fa fa-database"></i><?php echo $t['bind_repo']; ?></button>
|
||
<button class="btn-base btn-primary" onclick="openCreateModal()"><i class="fa fa-plus"></i><?php echo $t['create_container']; ?></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<div class="stat-card">
|
||
<div class="stat-label"><?php echo $t['stat_total']; ?></div>
|
||
<div class="stat-num" id="statTotal">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label"><?php echo $t['stat_running']; ?></div>
|
||
<div class="stat-num" id="statRunning">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label"><?php echo $t['stat_stopped']; ?></div>
|
||
<div class="stat-num" id="statStopped">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label"><?php echo $t['stat_image']; ?></div>
|
||
<div class="stat-num" id="statImage">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-bar">
|
||
<div class="tab-item active" data-tab="container"><?php echo $t['container_list']; ?></div>
|
||
<div class="tab-item" data-tab="image"><?php echo $t['image_list']; ?></div>
|
||
</div>
|
||
|
||
<div class="tab-panel active" id="panel-container">
|
||
<div class="tool-bar">
|
||
<input class="search-input" id="searchContainer" placeholder="<?php echo $t['search_placeholder']; ?>">
|
||
<div class="batch-group">
|
||
<button class="btn-base btn-outline" onclick="batchOp('restart')"><?php echo $t['batch_restart']; ?></button>
|
||
<button class="btn-base btn-outline" onclick="batchOp('stop')"><?php echo $t['batch_stop']; ?></button>
|
||
<button class="btn-base btn-danger" onclick="batchOp('rm')"><?php echo $t['batch_remove']; ?></button>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th><input type="checkbox" id="checkAll"></th>
|
||
<th><?php echo $t['container_id']; ?></th>
|
||
<th><?php echo $t['container_name']; ?></th>
|
||
<th><?php echo $t['image_name']; ?></th>
|
||
<th><?php echo $t['status']; ?></th>
|
||
<th><?php echo $t['port_mapping']; ?></th>
|
||
<th><?php echo $t['operate']; ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dockerTable">
|
||
<tr>
|
||
<td colspan="7">
|
||
<div class="empty-state">
|
||
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
|
||
<div><?php echo $t['loading']; ?></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-panel" id="panel-image">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th><?php echo $t['repo']; ?></th>
|
||
<th><?php echo $t['tag']; ?></th>
|
||
<th><?php echo $t['image_id']; ?></th>
|
||
<th><?php echo $t['size']; ?></th>
|
||
<th><?php echo $t['create_time']; ?></th>
|
||
<th><?php echo $t['operate']; ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="imageTable">
|
||
<tr>
|
||
<td colspan="6">
|
||
<div class="empty-state">
|
||
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
|
||
<div><?php echo $t['loading']; ?></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-mask" id="modal">
|
||
<div class="modal-box">
|
||
<div class="modal-head">
|
||
<span id="modalTitle">Info</span>
|
||
<span class="modal-close" onclick="closeModal()">×</span>
|
||
</div>
|
||
<div class="modal-body" id="modalBody"></div>
|
||
<div class="modal-footer">
|
||
<div style="color:#2563eb;font-size:13px;margin-right:12px;" id="execTip"><?php echo $t['tip_websocket']; ?></div>
|
||
<input class="cmd-input" id="cmdInput" placeholder="<?php echo $t['input_cmd']; ?>">
|
||
<button class="btn-base btn-primary" onclick="runExec()" id="singleExecBtn"><?php echo $t['execute']; ?></button>
|
||
<button class="btn-base btn-outline" onclick="closeModal()"><?php echo $t['close']; ?></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-mask" id="createModal">
|
||
<div class="modal-box" style="max-width:760px;">
|
||
<div class="modal-head">
|
||
<span><?php echo $t['create_container']; ?></span>
|
||
<span class="modal-close" onclick="closeCreateModal()">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-item">
|
||
<label>容器名称(留空自动生成)</label>
|
||
<input class="form-input" id="createName">
|
||
</div>
|
||
<div class="form-item">
|
||
<label>镜像(必填,例 nginx:stable)</label>
|
||
<input class="form-input" id="createImage">
|
||
</div>
|
||
<div class="form-item">
|
||
<label>启动命令(可选,覆盖镜像默认CMD)</label>
|
||
<input class="form-input" id="createCmd">
|
||
</div>
|
||
<div class="form-item">
|
||
<label>端口映射 格式 主机端口:容器端口,多个逗号分隔(例 8080:80)</label>
|
||
<input class="form-input" id="createPort">
|
||
</div>
|
||
<div class="form-item">
|
||
<label>CPU核心数(默认1)</label>
|
||
<input class="form-input" id="createCpu" value="1" type="number" min="1">
|
||
</div>
|
||
<div class="form-item">
|
||
<label>内存限制(默认512m,例 1g/2g)</label>
|
||
<input class="form-input" id="createMem" value="512m">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-base btn-primary" onclick="submitCreate()"><?php echo $t['execute']; ?></button>
|
||
<button class="btn-base btn-outline" onclick="closeCreateModal()"><?php echo $t['close']; ?></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-mask" id="repoModal">
|
||
<div class="modal-box" style="max-width:760px;">
|
||
<div class="modal-head">
|
||
<span><?php echo $t['bind_repo_title']; ?></span>
|
||
<span class="modal-close" onclick="closeRepoModal()">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-item">
|
||
<label><?php echo $t['repo_name']; ?></label>
|
||
<input class="form-input" id="repoName">
|
||
</div>
|
||
<div class="form-item">
|
||
<label><?php echo $t['repo_addr']; ?></label>
|
||
<input class="form-input" id="repoAddr" placeholder="harbor.example.com">
|
||
</div>
|
||
<div class="form-item">
|
||
<label><?php echo $t['repo_user']; ?></label>
|
||
<input class="form-input" id="repoUser">
|
||
</div>
|
||
<div class="form-item">
|
||
<label><?php echo $t['repo_pwd']; ?></label>
|
||
<input class="form-input" id="repoPwd" type="password">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-base btn-primary" onclick="submitBindRepo()"><?php echo $t['execute']; ?></button>
|
||
<button class="btn-base btn-outline" onclick="closeRepoModal()"><?php echo $t['close']; ?></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let socket = null;
|
||
let term = null;
|
||
const SOCKET_URL = "http://10.150.117.190:8090";
|
||
const WS_AUTH = {
|
||
user: "<?=$wsUser?>",
|
||
role: "<?=$wsRole?>",
|
||
ts: "<?=$wsTs?>",
|
||
sign: "<?=$wsSign?>",
|
||
clientIp: "<?=$wsClientIp?>"
|
||
};
|
||
|
||
function initSocket(){
|
||
socket = io(SOCKET_URL, {
|
||
auth: WS_AUTH,
|
||
transports: ['websocket']
|
||
});
|
||
socket.on("connect", function(){
|
||
console.log("WebSocket连接成功");
|
||
});
|
||
socket.on("connect_error", function(err){
|
||
console.error("WebSocket连接失败", err);
|
||
});
|
||
socket.on("terminal_output", function(res){
|
||
if(term) term.write(res.data);
|
||
});
|
||
socket.on("disconnect", ()=>{
|
||
if(term) term.write("\n\n🔌 终端连接已断开\n");
|
||
});
|
||
}
|
||
|
||
function closeTerminalSession(){
|
||
if(socket){
|
||
socket.emit("terminal_close");
|
||
}
|
||
if(term){
|
||
term.dispose();
|
||
term = null;
|
||
}
|
||
}
|
||
|
||
let currentFullId = '';
|
||
let opLock = false;
|
||
let refreshTimer = null;
|
||
let selectedContainers = [];
|
||
const BAN_CMD = ["rm","wget","curl","scp","chmod","chown","shutdown","systemctl","yum","apt","docker","nc"];
|
||
|
||
function showLoading(){ $("#globalLoading").css("display","flex"); }
|
||
function hideLoading(){ $("#globalLoading").hide(); }
|
||
|
||
function updateTime() {
|
||
const now = new Date();
|
||
const Y = now.getFullYear();
|
||
const M = String(now.getMonth() + 1).padStart(2, '0');
|
||
const D = String(now.getDate()).padStart(2, '0');
|
||
const h = String(now.getHours()).padStart(2, '0');
|
||
const m = String(now.getMinutes()).padStart(2, '0');
|
||
const s = String(now.getSeconds()).padStart(2, '0');
|
||
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
|
||
}
|
||
|
||
$("#langSwitch").change(function(){
|
||
const newLang = $(this).val();
|
||
$.post("./api/index.php",{act:"save_lang",lang:newLang},()=>{
|
||
location.href = "docker_mgr.php?_t="+new Date().getTime();
|
||
});
|
||
});
|
||
|
||
$(".tab-item").click(function(){
|
||
let tab = $(this).data("tab");
|
||
$(".tab-item").removeClass("active");
|
||
$(this).addClass("active");
|
||
$(".tab-panel").removeClass("active");
|
||
$("#panel-"+tab).addClass("active");
|
||
if(tab === "container") loadDockerList();
|
||
if(tab === "image") loadImageList();
|
||
});
|
||
|
||
$("#refreshAll").click(function(){
|
||
loadDockerList();
|
||
loadImageList();
|
||
});
|
||
|
||
function renderStatusTag(status){
|
||
let cls = "tag-gray";
|
||
let text = status;
|
||
switch(status){
|
||
case "running":
|
||
cls = "tag-success";
|
||
text = "Running";
|
||
break;
|
||
case "exited":
|
||
cls = "tag-danger";
|
||
text = "Stopped";
|
||
break;
|
||
}
|
||
return `<span class="${cls}">${text}</span>`;
|
||
}
|
||
|
||
function loadDockerList(){
|
||
showLoading();
|
||
$.ajax({
|
||
url: "./api/docker.php?act=list",
|
||
type:"GET",
|
||
xhrFields: {withCredentials:true},
|
||
dataType:"json",
|
||
success:function(res){
|
||
hideLoading();
|
||
let html = "";
|
||
let total = 0;
|
||
let running = 0;
|
||
let stopped = 0;
|
||
if(typeof res !== "object" || res.code === undefined){
|
||
html = `<tr><td colspan="7" class="empty-state" style="color:#dc2626">接口返回数据格式错误</td></tr>`;
|
||
}else if(res.code !== 0){
|
||
html = `<tr><td colspan="7" class="empty-state" style="color:#dc2626">${res.msg}</td></tr>`;
|
||
}else if(!res.list || res.list.length === 0){
|
||
html = `<tr><td colspan="7" class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div><?php echo $t['no_container']; ?></div></td></tr>`;
|
||
}else{
|
||
total = res.list.length;
|
||
res.list.forEach(item=>{
|
||
try{
|
||
if(item.status === "running") running++;
|
||
else if(item.status === "exited") stopped++;
|
||
let portText = item.ports || "-";
|
||
let statusHtml = renderStatusTag(item.status);
|
||
html += `<tr data-name="${item.name}" data-image="${item.image}">
|
||
<td><input type="checkbox" class="container-check" data-cid="${item.full_id}"></td>
|
||
<td title="点击复制完整ID" onclick="copyId('${item.full_id}')">${item.id}</td>
|
||
<td class="container-name">${item.name}</td>
|
||
<td>${item.image}</td>
|
||
<td>${statusHtml}</td>
|
||
<td>${portText}</td>
|
||
<td>
|
||
<div class="op-dropdown">
|
||
<button class="btn-base btn-outline op-trigger"><?php echo $t['op_menu']; ?></button>
|
||
<div class="op-dropdown-menu">
|
||
<div class="op-dropdown-item" onclick="openLogs('${item.full_id}','${item.name}')"><?php echo $t['view_log']; ?></div>
|
||
<div class="op-dropdown-item" onclick="openNetwork('${item.full_id}','${item.name}')"><?php echo $t['view_network']; ?></div>
|
||
<div class="op-dropdown-item" onclick="openExec('${item.full_id}','${item.name}')"><?php echo $t['enter_exec']; ?></div>
|
||
<div class="op-dropdown-item" onclick="dockerOperate('restart','${item.full_id}')"><?php echo $t['restart']; ?></div>
|
||
<div class="op-dropdown-item" onclick="dockerOperate('stop','${item.full_id}')"><?php echo $t['stop']; ?></div>
|
||
<div class="op-dropdown-item" onclick="dockerOperate('rm','${item.full_id}')"><?php echo $t['remove']; ?></div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}catch(e){}
|
||
});
|
||
}
|
||
$("#dockerTable").html(html);
|
||
$("#statTotal").text(total);
|
||
$("#statRunning").text(running);
|
||
$("#statStopped").text(stopped);
|
||
$(".op-trigger").click(function(e){
|
||
e.stopPropagation();
|
||
$(".op-dropdown-menu").hide();
|
||
$(this).next(".op-dropdown-menu").toggle();
|
||
});
|
||
$(document).click(()=>$(".op-dropdown-menu").hide());
|
||
$("#checkAll").change(function(){
|
||
$(".container-check").prop("checked", $(this).prop("checked"));
|
||
syncSelected();
|
||
});
|
||
$(".container-check").change(syncSelected);
|
||
$("#searchContainer").off("input").on("input",function(){
|
||
let kw = $(this).val().toLowerCase();
|
||
$("#dockerTable tr").each(function(){
|
||
let n = $(this).data("name")||"";
|
||
let img = $(this).data("image")||"";
|
||
if(n.toLowerCase().includes(kw) || img.toLowerCase().includes(kw)) $(this).show();
|
||
else $(this).hide();
|
||
})
|
||
})
|
||
},
|
||
error:function(xhr){
|
||
hideLoading();
|
||
let errMsg = "<?php echo $t['request_fail']; ?>";
|
||
if(xhr.status === 401) errMsg = "会话失效,请刷新页面重新登录";
|
||
$("#dockerTable").html(`<tr><td colspan="7" class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>${errMsg}</div></td></tr>`);
|
||
}
|
||
});
|
||
}
|
||
|
||
function loadImageList(){
|
||
showLoading();
|
||
$.ajax({
|
||
url: "./api/docker.php?act=image_list",
|
||
type:"GET",
|
||
xhrFields: {withCredentials:true},
|
||
dataType:"json",
|
||
success:function(res){
|
||
hideLoading();
|
||
let html = "";
|
||
if(typeof res !== "object" || res.code === undefined){
|
||
html = `<tr><td colspan="6" class="empty-state" style="color:#dc2626">镜像接口数据异常</td></tr>`;
|
||
}else if(res.code !== 0){
|
||
html = `<tr><td colspan="6" class="empty-state" style="color:#dc2626">${res.msg}</td></tr>`;
|
||
}else if(!res.list || res.list.length === 0){
|
||
html = `<tr><td colspan="6" class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div><?php echo $t['no_image']; ?></div></td></tr>`;
|
||
}else{
|
||
$("#statImage").text(res.list.length);
|
||
res.list.forEach(i=>{
|
||
html += `<tr>
|
||
<td>${i.repo}</td>
|
||
<td>${i.tag}</td>
|
||
<td title="点击复制完整ID" onclick="copyId('${i.full_id}')">${i.id}</td>
|
||
<td>${i.size}</td>
|
||
<td>${i.created}</td>
|
||
<td>
|
||
<div class="op-dropdown">
|
||
<button class="btn-base btn-outline op-trigger"><?php echo $t['op_menu']; ?></button>
|
||
<div class="op-dropdown-menu">
|
||
<div class="op-dropdown-item" onclick="removeImage('${i.full_id}')"><?php echo $t['remove_image']; ?></div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
});
|
||
}
|
||
$("#imageTable").html(html);
|
||
$(".op-trigger").click(function(e){
|
||
e.stopPropagation();
|
||
$(".op-dropdown-menu").hide();
|
||
$(this).next(".op-dropdown-menu").toggle();
|
||
});
|
||
$(document).click(()=>$(".op-dropdown-menu").hide());
|
||
},
|
||
error:function(xhr){
|
||
hideLoading();
|
||
$("#imageTable").html(`<tr><td colspan="6" class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div><?php echo $t['request_fail']; ?></div></td></tr>`);
|
||
}
|
||
});
|
||
}
|
||
|
||
function removeImage(imageId){
|
||
if(!confirm("<?php echo $t['confirm_remove_img']; ?>")) return;
|
||
showLoading();
|
||
$.post("./api/docker.php",{act:"remove_image",image_id:imageId},function(res){
|
||
hideLoading();
|
||
alert(res.msg || "<?php echo $t['op_success']; ?>");
|
||
loadImageList();
|
||
}).fail(function(){
|
||
hideLoading();
|
||
alert("<?php echo $t['op_failed']; ?>");
|
||
});
|
||
}
|
||
|
||
function copyId(fullId){
|
||
let inp = document.createElement("input");
|
||
inp.value = fullId;
|
||
document.body.appendChild(inp);
|
||
inp.select();
|
||
document.execCommand("copy");
|
||
document.body.removeChild(inp);
|
||
alert("完整ID已复制剪贴板");
|
||
}
|
||
function syncSelected(){
|
||
selectedContainers = [];
|
||
$(".container-check:checked").each(function(){
|
||
selectedContainers.push($(this).data("cid"));
|
||
})
|
||
}
|
||
|
||
function batchOp(action){
|
||
if(selectedContainers.length === 0){
|
||
alert("请先勾选容器");
|
||
return;
|
||
}
|
||
if(action === "rm" && !confirm("<?php echo $t['confirm_remove']; ?>")) return;
|
||
showLoading();
|
||
let idx = 0;
|
||
function next(){
|
||
if(idx >= selectedContainers.length){
|
||
hideLoading();
|
||
loadDockerList();
|
||
alert("批量操作完成");
|
||
return;
|
||
}
|
||
$.post("./api/docker.php",{act:action,container_id:selectedContainers[idx]},()=>{
|
||
idx++; next();
|
||
}).fail(()=>{idx++;next();})
|
||
}
|
||
next();
|
||
}
|
||
|
||
function dockerOperate(action,cid){
|
||
if(opLock) return;
|
||
if(action === "rm" && !confirm("<?php echo $t['confirm_remove']; ?>")) return;
|
||
opLock = true;
|
||
showLoading();
|
||
$("button").prop("disabled",true);
|
||
$.post("./api/docker.php",{act:action,container_id:cid},function(res){
|
||
hideLoading();
|
||
opLock = false;
|
||
$("button").prop("disabled",false);
|
||
alert(res.msg || "<?php echo $t['op_success']; ?>");
|
||
loadDockerList();
|
||
}).fail(function(){
|
||
hideLoading();
|
||
opLock = false;
|
||
$("button").prop("disabled",false);
|
||
alert("<?php echo $t['op_failed']; ?>");
|
||
});
|
||
}
|
||
|
||
function openLogs(cid,name){
|
||
currentFullId = cid;
|
||
closeTerminalSession();
|
||
$("#modalTitle").text(name + " - <?php echo $t['container_log']; ?>");
|
||
$("#modal").css("display","flex");
|
||
$("#modalBody").css("background","#111827").text("加载日志中...");
|
||
$("#cmdInput").show();
|
||
$("#singleExecBtn").show();
|
||
$("#execTip").text("<?php echo $t['tip_exec_once']; ?>");
|
||
$.ajax({
|
||
url: "./api/docker.php?act=logs&container_id="+cid+"&lines=500",
|
||
xhrFields: {withCredentials:true},
|
||
dataType:"json",
|
||
success:function(res){
|
||
$("#modalBody").text(res.data);
|
||
}
|
||
});
|
||
}
|
||
|
||
function openNetwork(cid,name){
|
||
currentFullId = cid;
|
||
closeTerminalSession();
|
||
$("#modalTitle").text(name + " - <?php echo $t['network_info']; ?>");
|
||
$("#modal").css("display","flex");
|
||
$("#modalBody").css("background","#111827").text("加载网络信息...");
|
||
$("#cmdInput").show();
|
||
$("#singleExecBtn").show();
|
||
$("#execTip").text("<?php echo $t['tip_exec_once']; ?>");
|
||
$.ajax({
|
||
url: "./api/docker.php?act=network&container_id="+cid,
|
||
xhrFields: {withCredentials:true},
|
||
dataType:"json",
|
||
success:function(res){
|
||
$("#modalBody").text(res.data);
|
||
}
|
||
});
|
||
}
|
||
|
||
function openExec(cid,name){
|
||
currentFullId = cid;
|
||
closeTerminalSession();
|
||
$("#modalTitle").text(name + " - <?php echo $t['enter_exec']; ?>");
|
||
$("#modal").css("display","flex");
|
||
$("#modalBody").css("background","#111827").empty();
|
||
$("#cmdInput").hide();
|
||
$("#singleExecBtn").hide();
|
||
$("#execTip").text("<?php echo $t['tip_websocket']; ?>");
|
||
|
||
term = new Terminal({
|
||
rows:24,
|
||
cols:80,
|
||
theme:{background:"#111827",foreground:"#e5e7eb",cursor:"#60a5fa"},
|
||
fontSize:13,
|
||
fontFamily:"Consolas,Monaco,monospace"
|
||
});
|
||
term.open(document.getElementById("modalBody"));
|
||
|
||
const resizeObserver = new ResizeObserver(()=>{
|
||
const width = Math.floor($("#modalBody").width() / 9);
|
||
const height = Math.floor($("#modalBody").height() / 18);
|
||
term.resize(width, height);
|
||
socket.emit("terminal_resize",{width:width,height:height});
|
||
});
|
||
resizeObserver.observe(document.getElementById("modalBody"));
|
||
|
||
term.onData(function(data){
|
||
socket.emit("terminal_input",{data:data});
|
||
});
|
||
|
||
socket.emit("terminal_start",{cid:cid});
|
||
}
|
||
|
||
function runExec(){
|
||
let cmd = $("#cmdInput").val().trim();
|
||
if(!cmd) return;
|
||
let hit = false;
|
||
BAN_CMD.forEach(b=>{if(cmd.includes(b)) hit=true;});
|
||
if(hit){
|
||
alert("禁止执行高危操作:"+BAN_CMD.join("、"));
|
||
return;
|
||
}
|
||
$.post("./api/docker.php",{act:"exec",container_id:currentFullId,cmd:cmd},function(res){
|
||
$("#modalBody").text(res.data);
|
||
}).fail(()=>alert("<?php echo $t['request_fail']; ?>"));
|
||
}
|
||
|
||
function closeModal(){
|
||
closeTerminalSession();
|
||
$("#modal").hide();
|
||
$("#modalBody").empty();
|
||
$("#modalTitle").text("");
|
||
$("#cmdInput").val("");
|
||
}
|
||
|
||
function openCreateModal(){
|
||
$("#createName").val("");
|
||
$("#createImage").val("");
|
||
$("#createCmd").val("");
|
||
$("#createPort").val("");
|
||
$("#createCpu").val("1");
|
||
$("#createMem").val("512m");
|
||
$("#createModal").css("display","flex");
|
||
}
|
||
function closeCreateModal(){
|
||
$("#createModal").hide();
|
||
}
|
||
function submitCreate(){
|
||
let name = $("#createName").val().trim();
|
||
let image = $("#createImage").val().trim();
|
||
let cmd = $("#createCmd").val().trim();
|
||
let portStr = $("#createPort").val().trim();
|
||
let cpu = $("#createCpu").val().trim() || "1";
|
||
let memory = $("#createMem").val().trim() || "512m";
|
||
if(!image){
|
||
alert("镜像不能为空");
|
||
return;
|
||
}
|
||
let ports = {};
|
||
if(portStr){
|
||
let arr = portStr.split(",");
|
||
arr.forEach(item=>{
|
||
let sp = item.split(":");
|
||
if(sp.length===2) ports[sp[1]] = sp[0];
|
||
});
|
||
}
|
||
let postBody = {
|
||
name:name,
|
||
image:image,
|
||
cmd:cmd||null,
|
||
ports:ports,
|
||
cpu:parseInt(cpu),
|
||
memory:memory
|
||
};
|
||
showLoading();
|
||
$.ajax({
|
||
url:"./api/docker.php?act=create",
|
||
method:"POST",
|
||
contentType:"application/json",
|
||
data:JSON.stringify(postBody),
|
||
xhrFields:{withCredentials:true},
|
||
dataType:"json",
|
||
success:function(res){
|
||
hideLoading();
|
||
alert(res.msg);
|
||
if(res.code===0){
|
||
closeCreateModal();
|
||
loadDockerList();
|
||
}
|
||
},
|
||
error:function(){
|
||
hideLoading();
|
||
alert("请求失败");
|
||
}
|
||
});
|
||
}
|
||
|
||
function openRepoModal(){
|
||
$("#repoName").val("");
|
||
$("#repoAddr").val("");
|
||
$("#repoUser").val("");
|
||
$("#repoPwd").val("");
|
||
$("#repoModal").css("display","flex");
|
||
}
|
||
function closeRepoModal(){
|
||
$("#repoModal").hide();
|
||
}
|
||
function submitBindRepo(){
|
||
let repoName = $("#repoName").val().trim();
|
||
let repoAddr = $("#repoAddr").val().trim();
|
||
let repoUser = $("#repoUser").val().trim();
|
||
let repoPwd = $("#repoPwd").val().trim();
|
||
if(!repoAddr){
|
||
alert("仓库地址不能为空");
|
||
return;
|
||
}
|
||
let postBody = {
|
||
repo_name:repoName,
|
||
repo_addr:repoAddr,
|
||
repo_user:repoUser,
|
||
repo_pwd:repoPwd
|
||
};
|
||
showLoading();
|
||
$.ajax({
|
||
url:"./api/docker.php?act=bind_repo",
|
||
method:"POST",
|
||
contentType:"application/json",
|
||
data:JSON.stringify(postBody),
|
||
xhrFields:{withCredentials:true},
|
||
dataType:"json",
|
||
success:function(res){
|
||
hideLoading();
|
||
alert(res.msg);
|
||
if(res.code===0){
|
||
closeRepoModal();
|
||
}
|
||
},
|
||
error:function(){
|
||
hideLoading();
|
||
alert("仓库绑定请求失败");
|
||
}
|
||
});
|
||
}
|
||
|
||
$("#logoutBtn").click(function(){
|
||
location.href = "logout.php";
|
||
});
|
||
|
||
$(function(){
|
||
initSocket();
|
||
updateTime();
|
||
setInterval(updateTime, 1000);
|
||
refreshTimer = setInterval(function(){
|
||
let activeTab = $(".tab-item.active").data("tab");
|
||
if(activeTab === "container") loadDockerList();
|
||
if(activeTab === "image") loadImageList();
|
||
}, 30000);
|
||
loadDockerList();
|
||
loadImageList();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|