Files
aler_dashboard_git/alert_dashboard/www/docker_mgr.php
T

1470 lines
48 KiB
PHP
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.
<?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; ?> &nbsp;|&nbsp; 接口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>