Files
aler_dashboard_git/docker_mgr.php.bak
2026-07-09 16:31:30 +08:00

1484 lines
47 KiB
Plaintext
Raw Permalink 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");
$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' => '强制删除',
'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' => '确认强制删除容器?操作不可恢复!',
'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' => '当前为单次命令执行,如需实时交互式终端需后端WebSocket扩展'
],
'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',
'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!',
'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' => 'Exec',
'stat_total' => 'Total Containers',
'stat_running' => 'Running',
'stat_stopped' => 'Stopped',
'stat_image' => 'Local Images',
'tip_exec_once' => 'Single command execution only, real-time terminal requires WebSocket backend'
]
];
$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();
?>
<!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">
<script src="/static/js/jquery.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;
}
/* 侧边栏现代化深色 #1e293b */
.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 固定高度64px */
.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;
display:flex;
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:20px 24px;
height:62vh;
overflow:auto;
background:#0d1117;
color:#c9d1d9;
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;
}
.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 $roleText; ?></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>
<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>
</tr>
</thead>
<tbody id="imageTable">
<tr>
<td colspan="5">
<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>
<!-- 日志/网络/Exec弹窗 -->
<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:#dc2626;font-size:13px;margin-right:12px;"><?php echo $t['tip_exec_once']; ?></div>
<input class="cmd-input" id="cmdInput" placeholder="<?php echo $t['input_cmd']; ?>">
<button class="btn-base btn-primary" onclick="runExec()"><?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>
<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>
<script>
d = '';
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();
});
// 【修复】容错端口格式化,防止报错阻塞JS渲染
function formatPorts(portObj){
try{
if(!portObj || Object.keys(portObj).length === 0) return "-";
let arr = [];
for(let cPort in portObj){
let hostList = portObj[cPort];
hostList.forEach(item=>{
arr.push(item.HostPort+":"+cPort);
})
}
return arr.join(", ");
}catch(e){
return "-";
}
}
// 状态标签渲染
function renderStatusTag(status){
let cls = "tag-gray";
let text = status;
if(status.startsWith("Up")){
cls = "tag-success";
}else if(status.startsWith("Exited")){
cls = "tag-danger";
}
return `<span class="${cls}">${text}</span>`;
}
// 加载容器列表
function loadDockerList(){
showLoading();
$.ajax({
url: "./api/docker.php?act=list",
type:"GET",
xhrFields: {withCredentials:true},
dataType:"json", // 强制解析JSON,捕获返回非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.startsWith("Up")) running++;
else stopped++;
let portText = formatPorts(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="5" class="empty-state" style="color:#dc2626">镜像接口数据异常</td></tr>`;
}else if(res.code !== 0){
html = `<tr><td colspan="5" class="empty-state" style="color:#dc2626">${res.msg}</td></tr>`;
}else if(!res.list || res.list.length === 0){
html = `<tr><td colspan="5" 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>
</tr>`;
});
}
$("#imageTable").html(html);
},
error:function(xhr){
hideLoading();
$("#imageTable").html(`<tr><td colspan="5" 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>`);
}
});
}
// 复制ID
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;
$("#modalTitle").text(name + " - <?php echo $t['container_log']; ?>");
$("#modal").css("display","flex");
$.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;
$("#modalTitle").text(name + " - <?php echo $t['network_info']; ?>");
$("#modal").css("display","flex");
$.ajax({
url: "./api/docker.php?act=network&container_id="+cid,
xhrFields: {withCredentials:true},
dataType:"json",
success:function(res){
$("#modalBody").text(res.data);
}
});
}
// Exec弹窗
function openExec(cid,name){
currentFullId = cid;
$("#modalTitle").text(name + " - <?php echo $t['exec_container']; ?>");
$("#modal").css("display","flex");
$("#modalBody").text("");
$("#cmdInput").val("ls -l");
}
// 执行命令前端拦截黑名单
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(){
$("#modal").hide();
$("#modalBody").empty();
$("#modalTitle").text("");
}
// 新建容器弹窗
function openCreateModal(){
$("#createName").val("");
$("#createImage").val("");
$("#createCmd").val("");
$("#createPort").val("");
$("#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();
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};
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 loadImageList(){
showLoading();
$.ajax({
url: "./api/docker.php?act=image_list",
type:"GET",
xhrFields: {withCredentials:true},
success:function(res){
hideLoading();
$("#statImage").text(res.list.length);
let html = "";
if(res.list.length === 0){
html = `<tr><td colspan="5" 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{
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>
</tr>`;
});
}
$("#imageTable").html(html);
},
error:function(){
hideLoading();
$("#imageTable").html(`<tr><td colspan="5" 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>`);
}
});
}
// 复制ID
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;
$("#modalTitle").text(name + " - <?php echo $t['container_log']; ?>");
$("#modal").css("display","flex");
$.ajax({
url: "./api/docker.php?act=logs&container_id="+cid+"&lines=500",
xhrFields: {withCredentials:true},
success:function(res){
$("#modalBody").text(res.data);
}
});
}
// 网络弹窗
function openNetwork(cid,name){
currentFullId = cid;
$("#modalTitle").text(name + " - <?php echo $t['network_info']; ?>");
$("#modal").css("display","flex");
$.ajax({
url: "./api/docker.php?act=network&container_id="+cid,
xhrFields: {withCredentials:true},
success:function(res){
$("#modalBody").text(res.data);
}
});
}
// Exec弹窗
function openExec(cid,name){
currentFullId = cid;
$("#modalTitle").text(name + " - <?php echo $t['exec_container']; ?>");
$("#modal").css("display","flex");
$("#modalBody").text("");
$("#cmdInput").val("ls -l");
}
// 执行命令前端拦截黑名单
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(){
$("#modal").hide();
$("#modalBody").empty();
$("#modalTitle").text("");
}
// 新建容器弹窗
function openCreateModal(){
$("#createName").val("");
$("#createImage").val("");
$("#createCmd").val("");
$("#createPort").val("");
$("#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();
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};
showLoading();
$.ajax({
url:"./api/docker.php?act=create",
method:"POST",
contentType:"application/json",
data:JSON.stringify(postBody),
xhrFields:{withCredentials:true},
success:function(res){
hideLoading();
alert(res.msg);
if(res.code===0){
closeCreateModal();
loadDockerList();
}
},
error:function(){
hideLoading();
alert("请求失败");
}
});
}
$("#logoutBtn").click(function(){
location.href = "logout.php";
});
// 页面初始化
$(function(){
updateTime();
setInterval(updateTime, 1000);
// 30秒自动刷新,实时同步容器存活状态
refreshTimer = setInterval(function(){
let activeTab = $(".tab-item.active").data("tab");
if(activeTab === "container") loadDockerList();
if(activeTab === "image") loadImageList();
}, 30000);
loadDockerList();
loadImageList();
});
</script>
</body>
</html>