Files
mixio/ejs/manage.ejs
2025-12-31 13:08:55 +08:00

661 lines
31 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.
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>MixIO管理后台</title>
<link rel="shortcut icon" href="img/shortcut.png"/>
<link rel="stylesheet" href="css/all.css">
<script src="js/jquery.min.js"></script>
<script src="js/lang.js?v=5"></script>
<script src="js/farbtastic.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/jquery.easing.min.js"></script>
<script src="js/echarts.min.js"></script>
<script src="js/mqtt.min.js"></script>
<link rel="stylesheet" href="css/dataTables.bootstrap4.min.css">
<script src="js/jquery.dataTables.min.js"></script>
<script src="js/dataTables.bootstrap4.min.js"></script>
<script src="js/tools.js?v=14"></script>
<script src="js/manage.js"></script>
</head>
<body id="page-top" class="sidebar-toggled" style="position:relative;overflow-x:hidden;color: black;">
<div id="wrapper">
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="index">
<div class="sidebar-brand-icon">
<i class="fa fa-cog"></i>
</div>
<div class="sidebar-brand-text mx-3" >MixIO Admin</div>
</a>
<hr class="sidebar-divider my-0">
<hr class="sidebar-divider">
<div class="sidebar-heading lang" key="MANAGE">
</div>
<li class="nav-item active" id="view1">
<a class="nav-link" href="/admin?page=basic" style="cursor: pointer;" id="manage_prj">
<i class="fa fa-fw fa-tachometer"></i>
<span class="lang" key="BASICADMIN"></span></a>
</li>
<li class="nav-item" id="view2">
<a class="nav-link" href="/admin?page=user" style="cursor: pointer;" id="manage_data">
<i class="fa fa-fw fa-database"></i>
<span class="lang" key="DATAADMIN"></span></a>
</li>
<li class="nav-item" id="view3">
<a class="nav-link" href="/admin?page=batch" style="cursor: pointer;" id="manage_strg">
<i class="fa fa-fw fa-user"></i>
<span class="lang" key="USERADMIN"></span></a>
</li>
<hr class="sidebar-divider">
</ul>
<div id="content-wrapper" class="d-flex flex-column">
<div id="content">
<nav class="navbar navbar-expand navbar-light bg-white topbar static-top shadow" style="display:flex;justify-content:space-between" id="project_nav">
<h1 class="d-sm-inline-block h3 mb-0 text-gray-800 lang" key="ADMIN" id="title" style="margin-left:10px;font-size:1.25rem;display:inline-block;"></h1>
</nav>
<div class="container-fluid" id="detail">
<div id="detail1" class="row">
<div class="col-xl-4 col-md-6 mb-4">
<div class="card shadow" style="margin-top:1.5rem;border-radius:10px">
<div class="card-body">
<div class="card border-left-primary h-100 py-2" style="margin-bottom:15px">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
平台类型</div>
<div class="h5 mb-0 font-weight-bold text-gray-800" id="platform"><%=platform%></div>
</div>
<div class="col-auto">
<i class="fa fa-code fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
<div class="card border-left-primary h-100 py-2" style="margin-bottom:15px">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
MixIO版本号</div>
<div class="h5 mb-0 font-weight-bold text-gray-800"><%=version%></div>
</div>
<div class="col-auto">
<i class="fa fa-code fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
<div class="card border-left-primary h-100 py-2" style="margin-bottom:15px">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Mixly编程服务器版本号</div>
<div class="h5 mb-0 font-weight-bold text-gray-800"><%=versionmixly%></div>
</div>
<div class="col-auto">
<i class="fa fa-code fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
<div class="card border-left-primary h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
服务器时钟</div>
<div class="h5 mb-0 font-weight-bold text-gray-800" id="time"></div>
</div>
<div class="col-auto">
<i class="fa fa-hourglass-half fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
<div class="row" style="display:flex;align-items:center;justify-content:center">
<button class="btn btn-lg btn-primary" style="margin-top:20px;margin-right:10px" onclick="fresh('mixio', '<%=platform%>')"><i class="fa fa-refresh"></i> 更新MixIO服务器</button>
</div>
<div class="row" style="display:flex;align-items:center;justify-content:center">
<button class="btn btn-lg btn-primary" style="margin-top:20px;margin-right:10px" onclick="fresh('mixly', 'all')"><i class="fa fa-refresh"></i> 更新Mixly编程服务器</button>
<!--
<% if (status=="运行中") { %>
<button class="btn btn-lg btn-danger" style="margin-top:20px;margin-left:10px" onclick="stopServer()"><i class="fa fa-ban"></i> 暂停服务器</button>
<%} else { %>
<button class="btn btn-lg btn-success" style="margin-top:20px;margin-left:10px" onclick="startServer()"><i class="fa fa-play"></i> 启动服务器</button>
<% } %>
-->
</div>
</div>
</div>
</div>
<div class="col-xl-8 col-md-6 mb-4">
<div class="card shadow" style="margin-top:1.5rem;border-radius:10px">
<div class="card-body" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
<table id="configTable">
<thead>
<tr>
<th scope="col" width="30%">配置项</th>
<th scope="col" width="30%">含义</th>
<th scope="col" width="10%">数据类型</th>
<th scope="col" width="30%">值</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button class="btn btn-lg btn-success" style="margin-top:20px;" onclick="saveAndRestart()" id="saveAndRestart"><i class="fa fa-check"></i> 保存设置并重新启动</button>
</div>
</div>
</div>
</div>
<div id="detail3" hidden>
<div class="col-xl-8 col-md-6 mb-4">
<div class="card shadow" style="margin-top:1.5rem;border-radius:10px">
<div class="card-body" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
<p>格式(每个账号一行): 账号,密码,密保问题,问题答案</p>
<textarea name="" id="acc" rows="10" style="margin-bottom: 20px;width:95%">12345@mixly.com,123456,你就读的班级是?,三年二班</textarea>
<div>
<button class="btn btn-lg btn-success" onclick="addAcc()"><i class="fa fa-check"></i> 确定添加</button>
</div>
</div>
</div>
</div>
</div>
<div id="detail2" hidden>
<div class="col-xl-12 col-md-6 mb-4">
<div class="card shadow" style="margin-top:1.5rem;border-radius:10px">
<div class="card-body" >
<table id="table">
<thead>
<td style="min-width:100px">
用户
</td>
<td style="min-width:100px">
项目数
</td>
<td style="min-width:100px">
消息量
</td>
<td style="min-width:300px">
执行操作
</td>
</thead>
<tbody id="tbody">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
input{
min-width:0!important;
height:30px
}
td{
min-height: 30px;
}
table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
th {
background-color: #2c3e50;
color: white;
font-weight: 500;
}
th:first-child {
border-top-left-radius: 8px;
}
th:last-child {
border-top-right-radius: 8px;
}
tr:nth-child(even) {
background-color: #f8f9fa;
}
tr:hover {
background-color: #e8f4fc;
}
td, th {
padding: 0.5rem !important;
vertical-align: middle !important;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 25px;
}
.badge-type {
font-size: 0.8em;
padding: 4px 8px;
border-radius: 4px;
}
.badge-string {
background-color: #3498db;
}
.badge-number {
background-color: #2ecc71;
}
.badge-boolean {
background-color: #e74c3c;
}
.form-control:focus, .form-select:focus {
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
border-color: #3498db;
}
.table-responsive {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
</style>
<script>
var clearMessage = function(userName){
$.get('clearMessage',{
"userName":userName
},function(res){
if(res == 1)
{
showtext("操作成功!")
setTimeout(function(){
window.location.href = window.location.href.split("?")[0] + "?page=user"
},1000)
}
else
{
showtext("操作失败")
}
})
}
var resetPassword = function(userName){
if (!confirm(`确定要重置用户 ${userName} 的密码吗?新密码将设置为 "123456"`)) {
return;
}
const modald = showmodaltext("<div style='text-align:center'><i class='fa fa-spin fa-cog' style='font-size:2rem;color:#4e73df'></i><p style='margin-top:6px;margin-bottom:0;color:#4e73df;font-size:1rem;font-weight:bold'>重置密码中...</p></div>");
$.get('resetf',{
"username":userName,
"password":"123456"
},function(res){
if(res == 1)
{
modald.close()
showtext("操作成功!")
setTimeout(function(){
window.location.href = window.location.href.split("?")[0] + "?page=user"
},1000)
}
else
{
modald.close()
showtext("操作失败")
}
})
}
var removeUser = function(userName){
if (!confirm(`确定要删除用户 ${userName} 的账号吗?此操作将不可恢复!`)) {
return;
}
const modald = showmodaltext("<div style='text-align:center'><i class='fa fa-spin fa-cog' style='font-size:2rem;color:#4e73df'></i><p style='margin-top:6px;margin-bottom:0;color:#4e73df;font-size:1rem;font-weight:bold'>重置密码中...</p></div>");
$.get('deletef',{
"username":userName
},function(res){
if(res == 1)
{
modald.close()
showtext("操作成功!")
setTimeout(function(){
window.location.href = window.location.href.split("?")[0] + "?page=user"
},1000)
}
else
{
modald.close()
showtext("操作失败")
}
})
}
$("#time").html(new Date().toLocaleTimeString())
setInterval(() => {
$("#time").html(new Date().toLocaleTimeString())
}, 1000);
var configs = <%-configs%>
$("#OFFLINE_MODE").bind('change',function(){
if($("#OFFLINE_MODE").prop("checked"))
{
$("#BAIDU_MAP_AK").attr("disabled","true")
$("#BAIDU_MAP_SERVER_AK").attr("disabled","true")
}
else
{
$("#BAIDU_MAP_AK").removeAttr("disabled")
$("#BAIDU_MAP_SERVER_AK").removeAttr("disabled")
}
})
var loadConfig = function(){
var mapping = {
"MIXIO_HTTP_PORT": "HTTP端口",
"MIXIO_HTTPS_PORT": "HTTPS端口",
"HTTPS_PRIVATE_PEM": "私钥链接(本地或在线地址)",
"HTTPS_CRT_FILE": "证书链接(本地或在线地址)",
"MIXIO_MQTT_PORT": "MQTT端口",
"MIXIO_WS_PORT": "MQTT-ws端口",
"MIXIO_WSS_PORT": "MQTT-wss端口",
"MAX_PROJECT_NUM_PER_USER": "用户的最大项目数",
"MAX_MESSAGE_PER_USER": "用户的最大离线消息存储数",
"MAX_MESSAGE_PER_SECOND": "用户每秒的最大消息数",
"ALLOW_REGISTER": "是否允许自主注册",
"ALLOW_HOOK": "是否允许离线存储消息",
"OFFLINE_MODE": "是否禁用天气/地图数据",
"BAIDU_MAP_AK": "百度地图客户端应用AK",
"BAIDU_MAP_SERVER_AK": "百度地图服务端应用AK",
"TENCENT_MAP_KEY": "腾讯地图key",
"BAIDU_STAT_LINK": "百度统计链接",
"ADMIN_USERNAME": "管理后台用户名",
"ADMIN_PASSWORD": "管理后台密码",
"STORAGE_ENGINE": "数据库引擎",
"MYSQL_HOST": "mysql地址",
"MYSQL_PORT": "mysql端口",
"MYSQL_USER": "mysql用户名",
"MYSQL_PASS": "mysql密码",
"MYSQL_DB": "mysql数据库名",
"FOOTER": "显示在首页的备案信息"
}
const tbody = document.querySelector('#configTable tbody');
tbody.innerHTML = '';
for (const [key, value] of Object.entries(configs)) {
const tr = document.createElement('tr');
// 配置名
const tdKey = document.createElement('td');
tdKey.textContent = key;
const tdName = document.createElement('td');
tdName.textContent = mapping[key];
// 数据类型
const tdType = document.createElement('td');
const type = typeof value;
const badge = document.createElement('span');
badge.className = `badge badge-type badge-${type}`;
badge.textContent = type;
tdType.appendChild(badge);
// 配置值
const tdValue = document.createElement('td');
let inputElement;
if (type === 'boolean') {
inputElement = document.createElement('select');
inputElement.className = 'form-control';
inputElement.innerHTML = `
<option value="true" ${value ? 'selected' : ''}>是 (true)</option>
<option value="false" ${!value ? 'selected' : ''}>否 (false)</option>
`;
} else if (type === 'number') {
inputElement = document.createElement('input');
inputElement.type = 'number';
inputElement.className = 'form-control';
inputElement.value = value;
inputElement.step = Number.isInteger(value) ? '1' : '0.01';
} else {
console.log(key)
inputElement = document.createElement('input');
inputElement.type = (mapping[key].indexOf('密码')==-1?'text':'password');
inputElement.className = 'form-control';
inputElement.value = value;
}
inputElement.setAttribute('data-key', key);
tdValue.appendChild(inputElement);
tr.appendChild(tdKey);
tr.appendChild(tdName);
tr.appendChild(tdType);
tr.appendChild(tdValue);
tbody.appendChild(tr);
}
}
loadConfig()
var saveAndRestart = function(){
var modald = showmodaltext("<div style='text-align:center'><i class='fa fa-spin fa-cog' style='font-size:2rem;color:#4e73df'></i><p style='margin-top:6px;margin-bottom:0;color:#4e73df;font-size:1rem;font-weight:bold'>"+JSLang[lang].loading2+"</p></div>")
const inputs = document.querySelectorAll('#configTable input, #configTable select');
configs = {};
inputs.forEach(input => {
const key = input.getAttribute('data-key');
const originalType = input.getAttribute('type');
if (originalType === 'text' || originalType === 'password') {
configs[key] = input.value;
} else if (originalType === 'number') {
configs[key] = Number(input.value);
} else {
configs[key] = input.value === 'true'
}
});
console.log(configs)
// 改为ajax, 超时设置为60s
$.ajax({
url: '/saveAndRestart',
type: 'GET',
data: {'configs':JSON.stringify(configs,null,2)},
timeout: 60000,
success: function(res){
if(res=="1")
{
showtext('保存成功, 正在重新启动服务器。')
}
else
showtext('保存失败')
modald.close()
},
error: function(res){
modald.close()
showtext('保存失败')
}
})
}
var stopServer = function(){
var modald = showmodaltext("<div style='text-align:center'><i class='fa fa-spin fa-cog' style='font-size:2rem;color:#4e73df'></i><p style='margin-top:6px;margin-bottom:0;color:#4e73df;font-size:1rem;font-weight:bold'>"+JSLang[lang].loading2+"</p></div>")
$.get('/stop',function(res){
modald.close()
if(res=="1")
{
window.location.href = window.location.href
}
else
showtext('状态异常')
})
}
var startServer = function(){
var modald = showmodaltext("<div style='text-align:center'><i class='fa fa-spin fa-cog' style='font-size:2rem;color:#4e73df'></i><p style='margin-top:6px;margin-bottom:0;color:#4e73df;font-size:1rem;font-weight:bold'>"+JSLang[lang].loading2+"</p></div>")
$.get('/start',function(res){
modald.close()
if(res=="1")
{
window.location.href = window.location.href
}
})
}
var fresh = function(product, platform){
var textarea = $("<textarea readonly cols='50' rows='20' style='color:white; background-color:black'></textarea>")
var add_text = function(text){
textarea.text(textarea.text() + text + "\n")
textarea.scrollTop(textarea[0].scrollHeight);
}
var d = dialog({
content: textarea
});
d.showModal();
add_text("发送更新请求中...")
fetch('/api/check-update', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
product: product,
platform: platform
})
})
.then((response) => response.json())
.then((result) => {
add_text("本地版本:" + result["localVersion"])
add_text("云端版本:" + result["cloudVersion"])
add_text("需要更新:" + result["cloudVersion"] && result["needsUpdate"])
var url = result["cloudFile"]
var error = result["error"]
if(error)
{
add_text(error)
}
if(result["cloudVersion"] && result["needsUpdate"])
{
const eventSource = new EventSource(`/api/download?url=${encodeURIComponent(url)}&cloudVersion=${result["cloudVersion"]}`);
add_text("下载开始...")
eventSource.onmessage = function(event) {
console.log(event)
const data = JSON.parse(event.data);
console.log(data)
if (data.type === 'progress') {
add_text(`下载中: ${data.progress}%`)
}
else if (data.type === 'unzip') {
add_text(`解压中...`)
}
else if (data.type === 'complete') {
eventSource.close();
if(data.version == "mixio"){
add_text(`${data.version}解压完成等待系统清理缓存大约需要30秒...`);
setTimeout(
function(){
alert("更新已完成请务必手动重启MIXIO服务请务必手动重启MIXIO服务如果是支持自动启动那么请直接重启服务器机器即可")
window.location.reload()
}
, 30000)
}
else{
alert("更新已完成!")
window.location.reload()
}
}
};
eventSource.onerror = function(error) {
console.log(error)
add_text('下载失败5秒后自动退出...');
setTimeout(function(){
d.close()
}, 5000)
eventSource.close();
};
}
else
{
add_text("已经是最新版本5秒后自动退出...")
setTimeout(function(){
d.close()
}, 5000)
}
})
.catch((error) => {
add_text(error);
});
}
var view = function(index){
a = [1,2,3]
for (i in a){
v = a[i]
if (index==v){
$("#detail"+v).removeAttr("hidden")
$("#view"+v).addClass("active")
}
else
{
$("#detail"+v).attr("hidden","hidden")
$("#view"+v).removeClass("active")
}
}
}
// 尝试获取page参数
if(window.location.href.split("?").length > 1){
var page = window.location.href.split("?")[1].split("=")[1]
if (page == "user")
view(2)
else if(page == "basic")
view(1)
else if(page == "batch")
view(3)
}
var addAcc = function(){
var modald = showmodaltext("<div style='text-align:center'><i class='fa fa-spin fa-cog' style='font-size:2rem;color:#4e73df'></i><p style='margin-top:6px;margin-bottom:0;color:#4e73df;font-size:1rem;font-weight:bold'>"+JSLang[lang].loading2+"</p></div>")
var acc = $("#acc").val()
accs = acc.split('\n')
acct = []
for(acc in accs){
if(accs[acc]!="")
acct.push(accs[acc])
}
var successCount = 0
var failCount = 0
var failInfo = []
for(i in acct){
info = acct[i].split(',')
if(info.length!=4)
{
failCount+=1
failInfo.push({
'account':info[0],
'reason':'格式不正确'
})
}
else
{
$.get('addAccount',{'userName':info[0],'password':info[1],'question':info[2],'answer':info[3]},function(res){
modald.close()
if(res=='1')
successCount+=1
else if(res=='2')
{
failCount+=1
failInfo.push({
'account':info[0],
'reason':'用户已存在'
})
}
else if(res=='3')
{
failCount+=1
failInfo.push({
'account':info[0],
'reason':'连接失败'
})
}
var s = "成功个数:"+successCount+"; 失败个数:"+failCount+"; 失败原因:"+JSON.stringify(failInfo)
showtext(s)
})
}
}
}
</script>
</body>