better-staridc-MNBT
1<?php
2/** Docker 管理视图(节点 / 用户 / 套餐) */
3mnbt_admin_include('head');
4$set = isset($_GET['gn']) ? $_GET['gn'] : 'node';
5?>
6<script type="text/javascript" src="<?= mnbt_asset_url('js/jquery-confirm/jquery-confirm.min.js') ?>"></script>
7<script type="text/javascript" src="<?= mnbt_asset_url('js/bootstrap-table/bootstrap-table.min.js') ?>"></script>
8<script type="text/javascript" src="<?= mnbt_asset_url('js/bootstrap-table/locale/bootstrap-table-zh-CN.min.js') ?>"></script>
9<style>
10.dk-admin-card { background:#fff; border-radius:6px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin:15px; }
11.dk-admin-card .card-tool { padding:12px 16px; border-bottom:1px solid #eee; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; }
12.dk-admin-card .card-body { padding:0; }
13.dk-tag-sm { display:inline-block; padding:2px 8px; border-radius:10px; font-size:12px; }
14.dk-tag-active { background:#e6f7ee; color:#2bb673; }
15.dk-tag-paused { background:#fff6e6; color:#e6a23c; }
16.dk-tag-expired { background:#fdeeee; color:#e0584b; }
17.dk-tag-pruned { background:#eee; color:#888; }
18.dk-tag-none { background:#eef0f4; color:#888; }
19</style>
20
21<?php if ($set === 'node'): ?>
22<!-- ============ Docker 节点管理 ============ -->
23<div class="dk-admin-card">
24 <div class="card-tool">
25 <h5 style="margin:0">Docker 节点(独立宝塔 Docker 面板实例)</h5>
26 <div style="display:flex;gap:8px">
27 <button type="button" class="btn btn-primary btn-sm js-create-tab" data-title="添加 Docker 节点" data-url="add.php?gn=dknode"><i class="mdi mdi-plus"></i> 添加节点</button>
28 <button class="btn btn-default btn-sm" onclick="dkNodeReload()">刷新</button>
29 </div>
30 </div>
31 <div class="card-body">
32 <table id="dkNodeTable"></table>
33 </div>
34</div>
35
36<!-- 节点容器查询区 -->
37<div class="dk-admin-card">
38 <div class="card-tool">
39 <h5 style="margin:0">节点容器查询</h5>
40 <div style="display:flex;gap:8px;align-items:center">
41 <select id="dkNodeSel" class="form-control" style="width:auto;min-width:220px"><option value="">请选择节点</option></select>
42 <button class="btn btn-primary btn-sm" onclick="dkNodeLoad()">查询</button>
43 </div>
44 </div>
45 <div class="card-body" style="padding:16px">
46 <div id="dkNodeInfo"></div>
47 <table class="table table-bordered" id="dkContainerTable" style="margin-top:14px">
48 <thead><tr><th>容器名</th><th>镜像</th><th>状态</th><th>端口</th><th>创建时间</th></tr></thead>
49 <tbody><tr><td colspan="5" class="text-center text-muted">请选择节点后查询</td></tr></tbody>
50 </table>
51 </div>
52</div>
53<script>
54$(function(){
55 $('#dkNodeTable').bootstrapTable({
56 url:'./ajax.php', method:'post', contentType:'application/x-www-form-urlencoded', pagination:false, search:true,
57 queryParams: function(){ return {gn:'docker_node_list'}; },
58 responseHandler: function(res){ return res.data || []; },
59 columns: [
60 {field:'id', title:'ID', width:50},
61 {field:'name', title:'节点名称'},
62 {field:'btip', title:'面板地址', formatter:function(v,row){ return v+':'+row.btdk+' '+(row.ptl==='true'?'(HTTPS)':'(HTTP)'); }},
63 {field:'btmy', title:'接口密钥', formatter:function(v){ return v?'<span class="text-muted">已设置</span>':'<span class="text-danger">未设置</span>'; }},
64 {field:'ktmy', title:'调用密钥', formatter:function(v){ return v?'<span class="text-muted">已设置</span>':'-'; }},
65 {field:'qk', title:'状态', formatter:function(v){ return v==='true'?'<span class="dk-tag-sm dk-tag-active">启用</span>':'<span class="dk-tag-sm dk-tag-none">禁用</span>'; }},
66 {field:'date', title:'添加时间'},
67 {field:'id', title:'操作', formatter:function(v){
68 return '<div class="btn-group btn-group-sm">'+
69 '<button class="btn btn-default" onclick="dkNodeEdit('+v+')">编辑</button>'+
70 '<button class="btn btn-danger" onclick="dkNodeDel('+v+')">删除</button>'+
71 '</div>';
72 }}
73 ]
74 });
75 dkLoadNodeSel();
76});
77function dkNodeReload(){ $('#dkNodeTable').bootstrapTable('refresh'); dkLoadNodeSel(); }
78function dkLoadNodeSel(){
79 $.post('./ajax.php', {gn:'docker_options'}, function(html){
80 var opt = JSON.parse(html);
81 var opts = '<option value="">请选择节点</option>';
82 (opt.nodes||[]).forEach(function(n){ opts += '<option value="'+n.id+'">'+n.name+' ('+n.btip+')</option>'; });
83 $('#dkNodeSel').html(opts);
84 });
85}
86function dkNodeEdit(id){
87 var row = null;
88 if (id) {
89 var rows = $('#dkNodeTable').bootstrapTable('getData') || [];
90 for (var i = 0; i < rows.length; i++) { if (rows[i].id == id) { row = rows[i]; break; } }
91 }
92 function v(f){ return row ? String(row[f] == null ? '' : row[f]).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>') : ''; }
93 $.confirm({
94 title: id?'编辑节点':'添加节点', columnClass:'m', content:
95 '<form class="form-horizontal">'+
96 '<input type="hidden" id="dk_nid" value="'+id+'">'+
97 '<div class="form-group"><label>节点名称 *</label><input class="form-control" id="dk_name" value="'+v('name')+'" placeholder="如:北京节点A"></div>'+
98 '<div class="form-group"><label>宝塔面板地址 *</label><input class="form-control" id="dk_btip" value="'+v('btip')+'" placeholder="IP 或域名"></div>'+
99 '<div class="form-group"><label>端口</label><input class="form-control" id="dk_btdk" value="'+(row?v('btdk'):'8888')+'"></div>'+
100 '<div class="form-group"><label>HTTPS</label><select class="form-control" id="dk_ptl"><option value="false"'+(row&&row.ptl==='false'?' selected':'')+'>否</option><option value="true"'+(row&&row.ptl==='true'?' selected':'')+'>是</option></select></div>'+
101 '<div class="form-group"><label>宝塔接口密钥 *</label><textarea class="form-control" id="dk_btmy" rows="2" placeholder="宝塔面板 API 密钥">'+v('btmy')+'</textarea></div>'+
102 '<div class="form-group"><label>调用密钥(外部API鉴权)</label><input class="form-control" id="dk_ktmy" value="'+v('ktmy')+'" placeholder="留空则不校验调用密钥"></div>'+
103 '<div class="form-group"><label>二级验证密钥</label><input class="form-control" id="dk_qmk" value="'+v('qmk')+'" placeholder="与调用密钥组合 md5 校验"></div>'+
104 '<div class="form-group"><label>启用</label><select class="form-control" id="dk_qk"><option value="true"'+(row&&row.qk==='true'?' selected':'')+'>启用</option><option value="false"'+(row&&row.qk==='false'?' selected':'')+'>禁用</option></select></div>'+
105 '</form>',
106 buttons:{ ok:{text:'保存',btnClass:'btn-primary',action:function(){
107 var d={gn:id?'docker_node_edit':'docker_node_add', id:id, name:$('#dk_name').val(), btip:$('#dk_btip').val(), btdk:$('#dk_btdk').val(), ptl:$('#dk_ptl').val(), btmy:$('#dk_btmy').val(), ktmy:$('#dk_ktmy').val(), qmk:$('#dk_qmk').val(), qk:$('#dk_qk').val()};
108 if(!d.name||!d.btip||!d.btmy){ $.alert('节点名、面板地址、接口密钥必填'); return false; }
109 $.post('./ajax.php', d, function(r){ var j=JSON.parse(r); $.alert(j.msg||j.code); if((j.msg||'').indexOf('成功')>=0) dkNodeReload(); });
110 return true;
111 }}, cancel:{text:'取消'} }
112 });
113}
114function dkNodeDel(id){ $.confirm({ title:'确认删除', content:'删除该 Docker 节点?节点下有用户时无法删除。', buttons:{ ok:{text:'删除',btnClass:'btn-danger',action:function(){ $.post('./ajax.php',{gn:'docker_node_del',id:id},function(r){var j=JSON.parse(r);$.alert(j.msg||j.code); if((j.msg||'').indexOf('成功')>=0) dkNodeReload();}); }}, cancel:{text:'取消'} } }); }
115function dkNodeLoad(){
116 var nid = $('#dkNodeSel').val();
117 if(!nid){ $.alert('请选择节点'); return; }
118 $('#dkNodeInfo').html('<i class="mdi mdi-loading mdi-spin"></i> 加载配置…');
119 $.post('./ajax.php', {gn:'docker_node_config', node_id:nid}, function(r){
120 var c = JSON.parse(r); var d = (c.data&&c.data.data)?c.data.data:c.data;
121 var installed = d && (d.docker_installed || (d.service_status&&d.service_status.docker));
122 $('#dkNodeInfo').html('<div class="alert '+(installed?'alert-success':'alert-warning')+'">Docker '+ (installed?'已安装':'未安装或异常') + (d&&d.service_status?(' · Compose: '+(d.service_status.docker_compose?'已安装':'未安装')):'') + '</div>');
123 });
124 $('#dkContainerTable tbody').html('<tr><td colspan="5" class="text-center"><i class="mdi mdi-loading mdi-spin"></i> 加载中…</td></tr>');
125 $.post('./ajax.php', {gn:'docker_node_containers', node_id:nid}, function(r){
126 var c = JSON.parse(r); var list = (c.data&&c.data.data)?c.data.data:(c.data||[]);
127 list = Array.isArray(list)?list:[];
128 if(!list.length){ $('#dkContainerTable tbody').html('<tr><td colspan="5" class="text-center text-muted">该节点暂无容器</td></tr>'); return; }
129 var rows = '';
130 list.forEach(function(x){
131 var name = (x.name||(x.Names&&x.Names[0])||''); if(name.charAt(0)==='/') name=name.slice(1);
132 rows += '<tr><td class="dk-mono">'+name+'</td><td class="dk-mono">'+(x.image||x.Image||'')+'</td><td>'+(x.status||x.State||'')+'</td><td class="dk-mono">'+(x.ports||x.Ports||'')+'</td><td class="dk-mono">'+(x.time||x.Created||'')+'</td></tr>';
133 });
134 $('#dkContainerTable tbody').html(rows);
135 });
136}
137</script>
138
139<?php elseif ($set === 'user'): ?>
140<!-- ============ Docker 用户管理 ============ -->
141<div class="dk-admin-card">
142 <div class="card-tool">
143 <h5 style="margin:0">Docker 用户</h5>
144 <button class="btn btn-primary btn-sm" onclick="dkUserEdit(0)"><i class="mdi mdi-plus"></i> 添加用户</button>
145 </div>
146 <div class="card-body">
147 <table id="dkUserTable"></table>
148 </div>
149</div>
150<script>
151function dkStatusTag(qk){
152 var map = {active:['正常','dk-tag-active'],paused:['已暂停','dk-tag-paused'],expired:['已到期','dk-tag-expired'],pruned:['已清理','dk-tag-pruned']};
153 var m = map[qk] || ['未知','dk-tag-none'];
154 return '<span class="dk-tag-sm '+m[1]+'">'+m[0]+'</span>';
155}
156$(function(){
157 $('#dkUserTable').bootstrapTable({
158 url: './ajax.php', method:'post', contentType:'application/x-www-form-urlencoded',
159 pagination:true, pageSize:15, pageList:[15,30,50], search:true, showRefresh:true, sidePagination:'client',
160 queryParams: function(){ return {gn:'docker_user_list'}; },
161 responseHandler: function(res){ return res.data || []; },
162 columns: [
163 {field:'id', title:'ID', width:60},
164 {field:'username', title:'账号'},
165 {field:'email', title:'邮箱'},
166 {field:'node_name', title:'节点'},
167 {field:'plan_name', title:'套餐'},
168 {field:'app_name', title:'应用'},
169 {field:'container_status', title:'容器状态', formatter:function(v){ var m={none:'未创建',creating:'创建中',running:'运行中',stopped:'已停止',failed:'失败'}; return '<span class="dk-tag-sm dk-tag-'+(v==='running'?'active':(v==='none'?'none':(v==='creating'?'paused':'expired')))+'">'+(m[v]||v)+'</span>'; }},
170 {field:'disk_usage', title:'磁盘用量', formatter:function(v){ v=parseInt(v,10)||0; if(v<=0) return '-'; if(v<1073741824) return (v/1048576).toFixed(1)+' MB'; return (v/1073741824).toFixed(2)+' GB'; }},
171 {field:'data', title:'开通'},
172 {field:'datae', title:'到期', formatter:function(v){ return v==='0000-00-00'?'永久':v; }},
173 {field:'qk', title:'状态', formatter:dkStatusTag},
174 {field:'id', title:'操作', formatter:function(v,row){
175 return '<div class="btn-group btn-group-sm">'+
176 '<button class="btn btn-default" onclick="dkUserEdit('+v+')">编辑</button>'+
177 '<button class="btn btn-default" onclick="dkUserReset('+v+')">改密</button>'+
178 (row.qk==='active'?'<button class="btn btn-warning" onclick="dkUserPause('+v+')">暂停</button>':'<button class="btn btn-success" onclick="dkUserResume('+v+')">恢复</button>')+
179 '<button class="btn btn-danger" onclick="dkUserDel('+v+')">删除</button>'+
180 '</div>';
181 }}
182 ]
183 });
184});
185function dkReload(){ $('#dkUserTable').bootstrapTable('refresh'); }
186function dkUserEdit(id){
187 $.post('./ajax.php', {gn:'docker_options'}, function(html){
188 var opt = JSON.parse(html);
189 var nodeOpts = (opt.nodes||[]).map(function(n){ return '<option value="'+n.id+'">'+n.name+' ('+n.btip+')</option>'; }).join('');
190 var planOpts = '<option value="0">无套餐</option>' + (opt.plans||[]).map(function(p){ var proxy = p.proxy_max && p.proxy_max!=='0' ? p.proxy_max+'代理/' : ''; return '<option value="'+p.id+'">'+p.name+' ('+p.cpu_max+'核/'+p.mem_max+'MB/'+(p.disk_max&&p.disk_max!=='0'?p.disk_max+'MB磁盘/':'')+proxy+'¥'+p.jg+')</option>'; }).join('');
191 $.confirm({
192 title: id?'编辑用户':'添加用户', columnClass:'m', content:
193 '<form class="form-horizontal">'+
194 '<input type="hidden" id="dk_uid" value="'+id+'">'+
195 (id?'':'<div class="form-group"><label>账号</label><input class="form-control" id="dk_username"></div>')+
196 (id?'':'<div class="form-group"><label>密码</label><input class="form-control" id="dk_password" type="password"></div>')+
197 '<div class="form-group"><label>邮箱</label><input class="form-control" id="dk_email"></div>'+
198 '<div class="form-group"><label>Docker 节点 *</label><select class="form-control" id="dk_ssbt">'+nodeOpts+'</select></div>'+
199 '<div class="form-group"><label>套餐</label><select class="form-control" id="dk_plan_id">'+planOpts+'</select></div>'+
200 '<div class="form-group"><label>到期(0000-00-00=永久)</label><input class="form-control" id="dk_datae" value="0000-00-00"></div>'+
201 (id?'<div class="form-group"><label>状态</label><select class="form-control" id="dk_qk"><option value="active">正常</option><option value="paused">暂停</option><option value="expired">到期</option></select></div>':'')+
202 '</form>',
203 buttons: {
204 ok: { text:'保存', btnClass:'btn-primary', action:function(){
205 var d = {gn:id?'docker_user_edit':'docker_user_add', id:id};
206 d.username=$('#dk_username').val(); d.password=$('#dk_password').val(); d.email=$('#dk_email').val();
207 d.ssbt=$('#dk_ssbt').val(); d.plan_id=$('#dk_plan_id').val(); d.datae=$('#dk_datae').val();
208 if(id) d.qk=$('#dk_qk').val();
209 if(!id && (!d.username || !d.password)){ $.alert('账号密码必填'); return false; }
210 $.post('./ajax.php', d, function(r){ var j=JSON.parse(r); $.alert(j.msg||j.code); if((j.msg||'').indexOf('成功')>=0) dkReload(); });
211 return true;
212 }},
213 cancel: { text:'取消' }
214 }
215 });
216 });
217}
218function dkUserReset(id){
219 $.confirm({ title:'重置密码', content:'<input class="form-control" id="dk_newpwd" placeholder="新密码">', buttons:{ ok:{text:'重置',btnClass:'btn-primary',action:function(){ var p=$('#dk_newpwd').val(); if(!p){$.alert('密码不能为空');return false;} $.post('./ajax.php',{gn:'docker_user_reset',id:id,password:p},function(r){var j=JSON.parse(r);$.alert(j.msg||j.code);}); return true; }}, cancel:{text:'取消'} } });
220}
221function dkUserDel(id){ $.confirm({ title:'确认删除', content:'删除用户不会自动删除其容器,确认删除?', buttons:{ ok:{text:'删除',btnClass:'btn-danger',action:function(){ $.post('./ajax.php',{gn:'docker_user_del',id:id},function(r){var j=JSON.parse(r);$.alert(j.msg||j.code); if((j.msg||'').indexOf('成功')>=0) dkReload();}); }}, cancel:{text:'取消'} } }); }
222function dkUserPause(id){ $.post('./ajax.php',{gn:'docker_user_pause',id:id},function(r){var j=JSON.parse(r);$.alert(j.msg||j.code); dkReload();}); }
223function dkUserResume(id){ $.post('./ajax.php',{gn:'docker_user_resume',id:id},function(r){var j=JSON.parse(r);$.alert(j.msg||j.code); dkReload();}); }
224</script>
225
226<?php elseif ($set === 'plan'): ?>
227<!-- ============ 套餐管理 ============ -->
228<div class="dk-admin-card">
229 <div class="card-tool">
230 <h5 style="margin:0">Docker 套餐</h5>
231 <button class="btn btn-primary btn-sm" onclick="dkPlanEdit(0)"><i class="mdi mdi-plus"></i> 添加套餐</button>
232 </div>
233 <div class="card-body">
234 <table id="dkPlanTable"></table>
235 </div>
236</div>
237<script>
238$(function(){
239 $('#dkPlanTable').bootstrapTable({
240 url:'./ajax.php', method:'post', contentType:'application/x-www-form-urlencoded', pagination:false, search:true,
241 queryParams: function(){ return {gn:'docker_plan_list'}; },
242 responseHandler: function(res){ return res.data || []; },
243 columns: [
244 {field:'id', title:'ID', width:60},
245 {field:'name', title:'套餐名'},
246 {field:'jc', title:'介绍', formatter:function(v){ return v||'-'; }},
247 {field:'cpu_max', title:'CPU核'},
248 {field:'mem_max', title:'内存MB'},
249 {field:'disk_max', title:'磁盘MB', formatter:function(v){ return v && v!=='0' ? v+' MB' : '不限制'; }},
250 {field:'proxy_max', title:'代理数', formatter:function(v){ return v && v!=='0' ? v+'个' : '不限制'; }},
251 {field:'jg', title:'价格'},
252 {field:'qk', title:'上架', formatter:function(v){ return v==='true'?'<span class="dk-tag-sm dk-tag-active">上架</span>':'<span class="dk-tag-sm dk-tag-none">下架</span>'; }},
253 {field:'id', title:'操作', formatter:function(v){ return '<div class="btn-group btn-group-sm"><button class="btn btn-default" onclick="dkPlanEdit('+v+')">编辑</button><button class="btn btn-danger" onclick="dkPlanDel('+v+')">删除</button></div>'; }}
254 ]
255 });
256});
257function dkPlanReload(){ $('#dkPlanTable').bootstrapTable('refresh'); }
258function dkPlanEdit(id){
259 $.confirm({
260 title: id?'编辑套餐':'添加套餐', columnClass:'m', content:
261 '<form class="form-horizontal">'+
262 '<input type="hidden" id="dk_pid" value="'+id+'">'+
263 '<div class="form-group"><label>套餐名</label><input class="form-control" id="dk_name"></div>'+
264 '<div class="form-group"><label>介绍</label><textarea class="form-control" id="dk_jc" rows="2"></textarea></div>'+
265 '<div class="form-group"><label>CPU 核上限</label><input class="form-control" id="dk_cpu" value="1" type="number" step="0.1"></div>'+
266 '<div class="form-group"><label>内存 MB 上限</label><input class="form-control" id="dk_mem" value="512" type="number" step="32"></div>'+
267 '<div class="form-group"><label>磁盘配额 MB(0=不限制)</label><input class="form-control" id="dk_disk" value="0" type="number" step="128"></div>'+
268 '<div class="form-group"><label>反向代理数量上限(0=不限制)</label><input class="form-control" id="dk_proxy" value="0" type="number" step="1" min="0"></div>'+
269 '<div class="form-group"><label>价格</label><input class="form-control" id="dk_jg" value="0"></div>'+
270 '<div class="form-group"><label>上架</label><select class="form-control" id="dk_qk"><option value="true">上架</option><option value="false">下架</option></select></div>'+
271 '</form>',
272 buttons:{ ok:{text:'保存',btnClass:'btn-primary',action:function(){
273 var d={gn:id?'docker_plan_edit':'docker_plan_add', id:id, name:$('#dk_name').val(), jc:$('#dk_jc').val(), cpu_max:$('#dk_cpu').val(), mem_max:$('#dk_mem').val(), disk_max:$('#dk_disk').val(), proxy_max:$('#dk_proxy').val(), jg:$('#dk_jg').val(), qk:$('#dk_qk').val()};
274 if(!d.name){ $.alert('套餐名必填'); return false; }
275 $.post('./ajax.php', d, function(r){ var j=JSON.parse(r); $.alert(j.msg||j.code); if((j.msg||'').indexOf('成功')>=0) dkPlanReload(); });
276 return true;
277 }}, cancel:{text:'取消'} }
278 });
279}
280function dkPlanDel(id){ $.confirm({ title:'确认删除', content:'删除套餐?', buttons:{ ok:{text:'删除',btnClass:'btn-danger',action:function(){ $.post('./ajax.php',{gn:'docker_plan_del',id:id},function(r){var j=JSON.parse(r);$.alert(j.msg||j.code); if((j.msg||'').indexOf('成功')>=0) dkPlanReload();}); }}, cancel:{text:'取消'} } }); }
281</script>
282
283<?php endif; ?>