chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
@@ -0,0 +1,74 @@
{extend name="layout"}
<div class="layui-container">
<div class="admin-title">ACL 权限规则</div>
<div class="layui-text" style="margin-bottom:10px;color:#999;">
需在"服务设置"中开启 ACL 才生效。按 sort 升序匹配,命中第一条即决定放行/拒绝;
主题支持通配符 <code>+ / #</code> 与占位符 <code>%u</code>(用户名) <code>%c</code>(clientId)。
</div>
<button class="layui-btn layui-btn-sm" onclick="editAcl()">+ 新增规则</button>
<table class="layui-table" style="margin-top:10px;">
<thead>
<tr><th>sort</th><th>目标</th><th>主题过滤器</th><th>权限</th><th>动作</th><th>备注</th><th>操作</th></tr>
</thead>
<tbody>
{foreach $list as $c}
<tr>
<td>{$c.sort}</td>
<td>{$c.target_type}{if $c.target}:{$c.target}{/if}</td>
<td>{$c.topic}</td>
<td>{if $c.access==1}订阅{elseif $c.access==2}发布{else}订阅+发布{/if}</td>
<td>{if $c.allow}<span class="layui-badge layui-bg-green">允许</span>{else}<span class="layui-badge">拒绝</span>{/if}</td>
<td>{$c.remark}</td>
<td>
<a class="layui-btn layui-btn-xs" onclick='editAcl({$c|json_encode})'>编辑</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" onclick="delAcl({$c.id})">删除</a>
</td>
</tr>
{/foreach}
</tbody>
</table>
{$list|raw}
</div>
<script>
layui.use(['layer', 'jquery'], function(){
var $ = layui.jquery, layer = layui.layer;
window.editAcl = function(row){
row = row || {};
var sel = function(name, val, opts){
var s = '<select name="'+name+'">';
opts.forEach(function(o){ s += '<option value="'+o[0]+'" '+(val==o[0]?'selected':'')+'>'+o[1]+'</option>'; });
return s + '</select>';
};
var html = '<form class="layui-form" style="padding:20px;" lay-filter="aclForm">'
+ '<input type="hidden" name="id" value="'+(row.id||'')+'">'
+ '<div class="layui-form-item"><label class="layui-form-label">目标类型</label><div class="layui-input-inline">'+sel('target_type', row.target_type||'all', [['all','全部'],['user','按用户名'],['client','按clientId']])+'</div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">目标值</label><div class="layui-input-inline"><input name="target" class="layui-input" value="'+(row.target||'')+'" placeholder="target_type=all 时留空"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">主题过滤器</label><div class="layui-input-inline"><input name="topic" class="layui-input" value="'+(row.topic||'')+'" placeholder="如 device/%c/# 或 sensor/+/temp"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">权限</label><div class="layui-input-inline">'+sel('access', row.access||3, [[1,'订阅'],[2,'发布'],[3,'订阅+发布']])+'</div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">动作</label><div class="layui-input-inline">'+sel('allow', (row.allow===undefined?1:row.allow), [[1,'允许'],[0,'拒绝']])+'</div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">排序</label><div class="layui-input-inline"><input name="sort" class="layui-input" value="'+(row.sort||0)+'"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">备注</label><div class="layui-input-inline"><input name="remark" class="layui-input" value="'+(row.remark||'')+'"></div></div>'
+ '</form>';
layer.open({type:1, title:(row.id?'编辑':'新增')+'规则', area:['520px','auto'], content:html, btn:['保存','取消'],
success:function(){ layui.form.render(); },
yes:function(idx){
var data = {};
$('[lay-filter=aclForm]').serializeArray().forEach(function(i){ data[i.name]=i.value; });
$.post('/mqttbroker/backend/saveAcl', data, function(res){
layer.msg(res.message||res.msg||'ok');
layer.close(idx); setTimeout(function(){location.reload();},600);
});
}
});
};
window.delAcl = function(id){
layer.confirm('确认删除该规则?', function(idx){
$.post('/mqttbroker/backend/deleteAcl', {id:id}, function(res){ layer.msg(res.message||'已删除'); setTimeout(function(){location.reload();},600); });
layer.close(idx);
});
};
});
</script>
@@ -0,0 +1,65 @@
{extend name="layout"}
<div class="layui-container">
<div class="admin-title">认证账号</div>
<div class="layui-text" style="margin-bottom:10px;color:#999;">
当"允许匿名连接"关闭时,客户端必须使用此处账号连接;超级用户跳过 ACL 校验。
</div>
<button class="layui-btn layui-btn-sm" onclick="editAuth()">+ 新增账号</button>
<table class="layui-table" style="margin-top:10px;">
<thead>
<tr><th>ID</th><th>用户名</th><th>超级用户</th><th>状态</th><th>备注</th><th>操作</th></tr>
</thead>
<tbody>
{foreach $list as $c}
<tr>
<td>{$c.id}</td>
<td>{$c.username}</td>
<td>{if $c.is_superuser}<span class="layui-badge layui-bg-orange"></span>{else}否{/if}</td>
<td>{if $c.status}<span class="layui-badge layui-bg-green">启用</span>{else}<span class="layui-badge">禁用</span>{/if}</td>
<td>{$c.remark}</td>
<td>
<a class="layui-btn layui-btn-xs" onclick='editAuth({$c|json_encode})'>编辑</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" onclick="delAuth({$c.id})">删除</a>
</td>
</tr>
{/foreach}
</tbody>
</table>
{$list|raw}
</div>
<script>
layui.use(['layer', 'jquery'], function(){
var $ = layui.jquery, layer = layui.layer;
window.editAuth = function(row){
row = row || {};
var html = '<form class="layui-form" style="padding:20px;" lay-filter="authForm">'
+ '<input type="hidden" name="id" value="'+(row.id||'')+'">'
+ '<div class="layui-form-item"><label class="layui-form-label">用户名</label><div class="layui-input-inline"><input name="username" class="layui-input" value="'+(row.username||'')+'"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">密码</label><div class="layui-input-inline"><input name="password" class="layui-input" placeholder="'+(row.id?'留空表示不修改':'必填')+'"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">超级用户</label><div class="layui-input-inline"><select name="is_superuser"><option value="0" '+(!row.is_superuser?'selected':'')+'>否</option><option value="1" '+(row.is_superuser==1?'selected':'')+'>是</option></select></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">状态</label><div class="layui-input-inline"><select name="status"><option value="1" '+(row.status!=0?'selected':'')+'>启用</option><option value="0" '+(row.status==0?'selected':'')+'>禁用</option></select></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">备注</label><div class="layui-input-inline"><input name="remark" class="layui-input" value="'+(row.remark||'')+'"></div></div>'
+ '</form>';
layer.open({type:1, title:(row.id?'编辑':'新增')+'账号', area:['480px','auto'], content:html, btn:['保存','取消'],
success:function(){ layui.form.render(); },
yes:function(idx){
var data = {};
$('[lay-filter=authForm]').serializeArray().forEach(function(i){ data[i.name]=i.value; });
$.post('/mqttbroker/backend/saveAuth', data, function(res){
layer.msg(res.message||res.msg||'ok');
if((res.code===0)||(res.code===1)) { layer.close(idx); setTimeout(function(){location.reload();},600); }
});
}
});
};
window.delAuth = function(id){
layer.confirm('确认删除该账号?', function(idx){
$.post('/mqttbroker/backend/deleteAuth', {id:id}, function(res){ layer.msg(res.message||'已删除'); setTimeout(function(){location.reload();},600); });
layer.close(idx);
});
};
});
</script>
@@ -0,0 +1,22 @@
<div class="layui-container">
<div class="admin-title">客户端连接</div>
<table class="layui-table">
<thead>
<tr><th>ID</th><th>ClientID</th><th>用户名</th><th>IP</th><th>状态</th><th>连接时间</th></tr>
</thead>
<tbody>
{foreach $list as $c}
<tr>
<td>{$c.id}</td>
<td>{$c.client_id}</td>
<td>{$c.username}</td>
<td>{$c.ip}</td>
<td>{$c.status|default=0}</td>
<td>{if !empty($c.create_at)}{$c.create_at|date='Y-m-d H:i:s'}{else}-{/if}</td>
</tr>
{/foreach}
</tbody>
</table>
{$list|raw}
</div>
@@ -0,0 +1,53 @@
<div class="layui-container">
<div class="admin-title">MQTT 代理概览</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">在线连接</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;color:#1E9FFF;">{$stats.online}</div>
</div>
</div>
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">累计连接</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;">{$stats.total}</div>
</div>
</div>
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">消息总数</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;">{$stats.messages}</div>
</div>
</div>
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">订阅关系</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;">{$stats.topics}</div>
</div>
</div>
</div>
<div class="admin-title">最近消息</div>
<table class="layui-table">
<thead>
<tr><th>ID</th><th>主题</th><th>来源</th><th>内容</th><th>时间</th></tr>
</thead>
<tbody>
{foreach $recent as $m}
<tr>
<td>{$m.id}</td>
<td>{$m.topic}</td>
<td>{$m.source}</td>
<td>{$m.payload}</td>
<td>{if !empty($m.create_at)}{$m.create_at|date='Y-m-d H:i:s'}{else}-{/if}</td>
</tr>
{/foreach}
</tbody>
</table>
<div class="layui-text" style="margin-top:15px;color:#999;">
启动 Broker<code>php think mqttbroker:start</code>(需在命令行运行常驻进程)
</div>
</div>
@@ -0,0 +1,31 @@
<div class="layui-container">
<div class="admin-title">消息日志</div>
<form class="layui-form" action="" style="margin-bottom:10px;">
<div class="layui-inline">
<input type="text" name="topic" value="{$topic}" placeholder="按主题搜索" class="layui-input">
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit>搜索</button>
</div>
</form>
<table class="layui-table">
<thead>
<tr><th>ID</th><th>ClientID</th><th>主题</th><th>QoS</th><th>内容</th><th>来源</th><th>时间</th></tr>
</thead>
<tbody>
{foreach $list as $m}
<tr>
<td>{$m.id}</td>
<td>{$m.client_id}</td>
<td>{$m.topic}</td>
<td>{$m.qos}</td>
<td>{$m.payload}</td>
<td>{$m.source}</td>
<td>{if !empty($m.create_at)}{$m.create_at|date='Y-m-d H:i:s'}{else}-{/if}</td>
</tr>
{/foreach}
</tbody>
</table>
{$list|raw}
</div>
@@ -0,0 +1,45 @@
<div class="layui-container">
<div class="admin-title">发布消息</div>
<form class="layui-form" id="publishForm">
<div class="layui-form-item">
<label class="layui-form-label">主题</label>
<div class="layui-input-block">
<input type="text" name="topic" required lay-verify="required" placeholder="如 sensor/room1/temp" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">内容</label>
<div class="layui-input-block">
<textarea name="payload" placeholder="消息内容" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">QoS</label>
<div class="layui-input-block">
<select name="qos">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="publish">发布</button>
</div>
</div>
</form>
</div>
<script>
layui.use(['form', 'jquery'], function(){
var form = layui.form, $ = layui.jquery;
form.on('submit(publish)', function(data){
$.post('/mqttbroker/backend/publish', data.field, function(res){
layer.msg(res.message || res.msg || '已提交');
});
return false;
});
});
</script>
@@ -0,0 +1,79 @@
{extend name="layout"}
<div class="layui-container">
<div class="admin-title">转发规则(规则引擎 / 桥接到 EMQX)</div>
<div class="layui-text" style="margin-bottom:10px;color:#999;">
匹配"源主题过滤器"的消息,将被转发到外部 MQTT Broker(如 EMQX)。目标主题支持
<code>${topic}</code> 变量(沿用原主题)。规则最多 30s 后在 Broker 生效。
转发依赖 <code>workerman/mqtt</code> 客户端,未安装时自动跳过。
</div>
<button class="layui-btn layui-btn-sm" onclick="editRule()">+ 新增规则</button>
<table class="layui-table" style="margin-top:10px;">
<thead>
<tr><th>名称</th><th>源主题过滤器</th><th>目标Broker</th><th>目标主题</th><th>QoS</th><th>状态</th><th>操作</th></tr>
</thead>
<tbody>
{foreach $list as $r}
<tr>
<td>{$r.name}</td>
<td>{$r.source_filter}</td>
<td>{$r.target_broker}</td>
<td>{if !empty($r.target_topic)}{$r.target_topic}{else/}${topic}{/if}</td>
<td>{$r.target_qos}</td>
<td>{if $r.enabled}<span class="layui-badge layui-bg-green">启用</span>{else}<span class="layui-badge">停用</span>{/if}</td>
<td>
<a class="layui-btn layui-btn-xs" onclick='editRule({$r|json_encode})'>编辑</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" onclick="delRule({$r.id})">删除</a>
</td>
</tr>
{/foreach}
</tbody>
</table>
{$list|raw}
</div>
<script>
layui.use(['layer', 'jquery'], function(){
var $ = layui.jquery, layer = layui.layer;
window.editRule = function(row){
row = row || {};
var sel = function(name, val, opts){
var s = '<select name="'+name+'">';
opts.forEach(function(o){ s += '<option value="'+o[0]+'" '+(val==o[0]?'selected':'')+'>'+o[1]+'</option>'; });
return s + '</select>';
};
var html = '<form class="layui-form" style="padding:20px;" lay-filter="ruleForm">'
+ '<input type="hidden" name="id" value="'+(row.id||'')+'">'
+ '<div class="layui-form-item"><label class="layui-form-label">规则名称</label><div class="layui-input-inline"><input name="name" class="layui-input" value="'+(row.name||'')+'"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">源主题过滤器</label><div class="layui-input-inline"><input name="source_filter" class="layui-input" value="'+(row.source_filter||'')+'" placeholder="如 sensor/+/temp"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">目标类型</label><div class="layui-input-inline">'+sel('target_type', row.target_type||'mqtt', [['mqtt','MQTT Broker']])+'</div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">目标Broker</label><div class="layui-input-inline"><input name="target_broker" class="layui-input" value="'+(row.target_broker||'')+'" placeholder="host:port,如 192.168.1.10:1883"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">目标主题</label><div class="layui-input-inline"><input name="target_topic" class="layui-input" value="'+(row.target_topic||'')+'" placeholder="${topic} 表示沿用原主题"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">目标QoS</label><div class="layui-input-inline">'+sel('target_qos', (row.target_qos===undefined?0:row.target_qos), [[0,'0'],[1,'1'],[2,'2']])+'</div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">ClientID</label><div class="layui-input-inline"><input name="target_clientid" class="layui-input" value="'+(row.target_clientid||'')+'" placeholder="可选"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">用户名</label><div class="layui-input-inline"><input name="target_username" class="layui-input" value="'+(row.target_username||'')+'" placeholder="可选"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">密码</label><div class="layui-input-inline"><input name="target_password" type="password" class="layui-input" value="'+(row.target_password||'')+'" placeholder="可选"></div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">状态</label><div class="layui-input-inline">'+sel('enabled', (row.enabled===undefined?1:row.enabled), [[1,'启用'],[0,'停用']])+'</div></div>'
+ '<div class="layui-form-item"><label class="layui-form-label">备注</label><div class="layui-input-inline"><input name="remark" class="layui-input" value="'+(row.remark||'')+'"></div></div>'
+ '</form>';
layer.open({type:1, title:(row.id?'编辑':'新增')+'转发规则', area:['560px','auto'], content:html, btn:['保存','取消'],
success:function(){ layui.form.render(); },
yes:function(idx){
var data = {};
$('[lay-filter=ruleForm]').serializeArray().forEach(function(i){ data[i.name]=i.value; });
$.post('/mqttbroker/backend/saveRule', data, function(res){
layer.msg(res.message||res.msg||'ok');
layer.close(idx); setTimeout(function(){location.reload();},600);
});
}
});
};
window.delRule = function(id){
layer.confirm('确认删除该规则?', function(idx){
$.post('/mqttbroker/backend/deleteRule', {id:id}, function(res){ layer.msg(res.message||'已删除'); setTimeout(function(){location.reload();},600); });
layer.close(idx);
});
};
});
</script>
@@ -0,0 +1,40 @@
<div class="layui-card-body">
<div class="admin-title">服务设置</div>
<form class="layui-form" id="settingForm">
{foreach $def as $item}
<div class="layui-form-item">
<label class="layui-form-label">{$item.title}</label>
<div class="layui-input-inline">
{if isset($item.options) && $item.options}
<select name="{$item.name}">
{foreach $item.options as $k=>$v}
<option value="{$k}" {if (isset($saved[$item['name']]) && $saved[$item['name']]==$k)}selected{/if}>{$v}</option>
{/foreach}
</select>
{else}
<input type="text" name="{$item.name}" value="{$saved[$item['name']] ?? $item.value}" class="layui-input">
{/if}
</div>
{if isset($item.tip) && $item.tip}<div class="layui-form-mid layui-word-aux">{$item.tip}</div>{/if}
</div>
{/foreach}
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="save">保存</button>
</div>
</div>
</form>
</div>
<script>
layui.use(['form', 'jquery'], function(){
var form = layui.form, $ = layui.jquery;
form.on('submit(save)', function(data){
$.post('/mqttbroker/backend/setting', data.field, function(res){
layer.msg(res.message || '已保存');
});
return false;
});
});
</script>
@@ -0,0 +1,80 @@
<div class="layui-container">
<div class="admin-title">MQTT 实时运行监控
<span class="layui-badge layui-bg-blue" id="live" style="margin-left:10px;">实时</span>
<span style="float:right;color:#999;font-size:12px;">最后刷新:<span id="lastSync">--</span></span>
</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">在线连接</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;color:#1E9FFF;" id="m_online">0</div></div>
</div>
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">会话总数</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;" id="m_total">0</div></div>
</div>
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">订阅关系</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;color:#16a34a;" id="m_subs">0</div></div>
</div>
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">运行时长</div>
<div class="layui-card-body" style="font-size:30px;font-weight:bold;" id="m_uptime">00:00:00</div></div>
</div>
</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">累计接收</div>
<div class="layui-card-body" style="font-size:26px;font-weight:bold;" id="m_recv">0</div></div>
</div>
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">累计发送</div>
<div class="layui-card-body" style="font-size:26px;font-weight:bold;" id="m_sent">0</div></div>
</div>
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">保留消息</div>
<div class="layui-card-body" style="font-size:26px;font-weight:bold;" id="m_retained">0</div></div>
</div>
<div class="layui-col-md3">
<div class="layui-card"><div class="layui-card-header">快照时间</div>
<div class="layui-card-body" style="font-size:18px;" id="m_ts">--</div></div>
</div>
</div>
<div class="layui-text" style="margin-top:10px;color:#999;">
指标由 Broker 进程每 2 秒写入数据库,本页每 3 秒轮询刷新(跨进程安全,Windows 调试与 Linux 生产通用)。
若长时间显示 0,请确认 Broker 已启动:<code>php think mqttbroker:start</code>
</div>
</div>
<script>
layui.use(['jquery'], function(){
var $ = layui.jquery;
function fmtUptime(s){
s = parseInt(s||0);
var h = Math.floor(s/3600), m = Math.floor((s%3600)/60), ss = s%60;
var p = function(n){ return (n<10?'0':'')+n; };
return p(h)+':'+p(m)+':'+p(ss);
}
function load(){
$.getJSON('/mqttbroker/backend/stats', function(res){
if (res.code !== 0 || !res.data) return;
var d = res.data;
$('#m_online').text(d.db_online != null ? d.db_online : d.clients_online);
$('#m_total').text(d.clients_total);
$('#m_subs').text(d.subscriptions);
$('#m_uptime').text(fmtUptime(d.uptime));
$('#m_recv').text(d.messages_received);
$('#m_sent').text(d.messages_sent);
$('#m_retained').text(d.retained);
$('#m_ts').text(new Date((d.timestamp||0)*1000).toLocaleString());
$('#lastSync').text(new Date().toLocaleTimeString());
$('#live').text('实时●');
}).fail(function(){ $('#live').text('断开'); });
}
load();
setInterval(load, 3000);
});
</script>