feat(mcp): MCP 密钥管理后台(CURD 重组 Base/App,密钥仅创建时明文展示)

This commit is contained in:
augushong
2026-08-16 20:55:02 +08:00
parent 15a6f2d841
commit 918d1d4dd5
15 changed files with 593 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'system.mcp_key/index',
addUrl: 'system.mcp_key/add' + location.search,
editUrl: 'system.mcp_key/edit',
readUrl: 'system.mcp_key/read',
deleteUrl: 'system.mcp_key/delete',
exportUrl: 'system.mcp_key/export',
modifyUrl: 'system.mcp_key/modify',
};

View File

@@ -0,0 +1,40 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">密钥名称</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入密钥名称" value="{$Request.param.title|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
{foreach $select_list_status as $k=>$v}
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$Request.param.status"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<textarea name="remark" class="layui-textarea" placeholder="请输入备注">{$Request.param.remark|default=''}</textarea>
</div>
</div>
<blockquote class="layui-elem-quote layui-quote-nm">
密钥由服务端自动生成,提交后仅展示一次,请立即复制保存。
</blockquote>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,43 @@
$(function(){
// add 成功响应的 data 携带一次性明文密钥secret_key
// 用独立 layer.alert 展示 + 复制按钮,替代默认的"成功提示后直接关窗"流程。
ua.listen(null, function (res) {
var secretKey = res.data && res.data.secret_key ? res.data.secret_key : '';
if (secretKey) {
var html = ''
+ '密钥已创建,<span style="color:#e74c3c;font-weight:bold;">仅展示一次,请立即复制保存</span>'
+ '<br><br>'
+ '<span style="user-select:all;word-break:break-all;font-family:monospace;font-size:14px;background:#f2f2f2;padding:8px 12px;display:inline-block;max-width:100%;">' + secretKey + '</span>'
+ '&nbsp;&nbsp;<button type="button" class="layui-btn layui-btn-primary layui-btn-xs" data-toggle="copy-text" data-clipboard-text="' + secretKey + '"><i class="fa fa-copy"></i> 复制</button>'
+ '<br><br>'
+ '<span style="color:#999;">关闭后将无法再次查看明文,遗失只能删除后重建。</span>';
layer.alert(html, {
title: '密钥创建成功',
icon: 1,
area: ['520px', 'auto'],
success: function (layero) {
// 弹层内绑定复制按钮ClipboardJS
ua.api.copyText(layero);
}
}, function (index) {
layer.close(index);
// 关闭密钥弹窗后再走默认的关窗/刷新流程
if (ua.checkMobile()) {
location.href = ua.url(init.indexUrl);
} else {
ua.api.closeCurrentOpen({ refreshTable: true });
}
});
return false;
}
// 兜底:无 secret_key 时走默认成功流程
ua.msg.success(res.msg || '保存成功', function () {
if (ua.checkMobile()) {
location.href = ua.url(init.indexUrl);
} else {
ua.api.closeCurrentOpen({ refreshTable: true });
}
});
return false;
});
})

View File

@@ -0,0 +1,40 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">密钥名称</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入密钥名称" value="{$row.title|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
{foreach $select_list_status as $k=>$v}
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$row.status"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<textarea name="remark" class="layui-textarea" placeholder="请输入备注">{$row.remark|raw|default=''}</textarea>
</div>
</div>
<blockquote class="layui-elem-quote layui-quote-nm">
密钥内容不支持修改;如需更换密钥,请删除后重新创建。
</blockquote>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,12 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-index="{:auth('system.mcp_key/index')}"
data-auth-add="{:auth('system.mcp_key/add')}"
data-auth-edit="{:auth('system.mcp_key/edit')}"
data-auth-delete="{:auth('system.mcp_key/delete')}"
data-auth-modify="{:auth('system.mcp_key/modify')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@@ -0,0 +1,52 @@
$(function(){
ua.table.render({
init: init,
toolbar: ['refresh', 'add', 'delete'],
cols: [[
{type: 'checkbox'},
{field: 'id', title: 'ID', width: 80},
{field: 'title', title: '密钥名称', minWidth: 150},
{
// 密钥脱敏展示:前缀 + ****(明文与完整哈希均不出现在列表)
field: 'key_prefix',
title: '密钥',
width: 200,
search: false,
templet: function (d) {
return d.key_prefix ? d.key_prefix + '****' : '-';
}
},
{field: 'bind_admin_username', title: '创建者', width: 120, search: false},
{field: 'use_num', title: '调用次数', width: 100, search: false},
{
field: 'last_use_time',
title: '最后使用时间',
width: 170,
search: false,
templet: function (d) {
if (!d.last_use_time || d.last_use_time == 0) {
return '<span class="layui-text-em">未使用</span>';
}
var date = new Date(d.last_use_time * 1000);
var Y = date.getFullYear();
var m = ('0' + (date.getMonth() + 1)).slice(-2);
var day = ('0' + date.getDate()).slice(-2);
var H = ('0' + date.getHours()).slice(-2);
var i = ('0' + date.getMinutes()).slice(-2);
var s = ('0' + date.getSeconds()).slice(-2);
return Y + '-' + m + '-' + day + ' ' + H + ':' + i + ':' + s;
}
},
{field: 'status', search: 'select', selectList: ua.getDataBrage('select_list_status'), title: '状态', width: 100, templet: ua.table.switch},
{field: 'remark', title: '备注', templet: ua.table.text},
{
width: 150, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
'edit',
'delete'
]
},
]],
});
ua.listen();
})

View File

@@ -0,0 +1,109 @@
<div class="layuimini-container detail-container">
<div class="layuimini-main">
<div class="layui-card detail-card">
<div class="layui-card-header detail-header">
<div class="layui-row">
<div class="layui-col-md9">
<h2 class="detail-title">#{$row.id} {$row.title}</h2>
<div class="detail-id">ID: {$row.id}</div>
</div>
<div class="layui-col-md3 text-right detail-actions">
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
<button class="layui-btn" onclick="location.href='{:url("edit", ["id" => $row.id])}'">编辑</button>
</div>
</div>
</div>
<div class="layui-card-body detail-content">
<div class="layui-row layui-col-space12">
<!-- 左侧主体内容 -->
<div class="layui-col-md8 detail-main">
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">密钥名称</div>
<div class="detail-field-value">
{notempty name="row.title"}
{$row.title}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">密钥(脱敏)</div>
<div class="detail-field-value">
{notempty name="row.key_prefix"}
{$row.key_prefix}****
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">绑定创建者ID</div>
<div class="detail-field-value">
{notempty name="row.bind_admin_id"}
{$row.bind_admin_id}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">备注</div>
<div class="detail-field-value" style="white-space: pre-wrap;">
{notempty name="row.remark"}
{$row.remark|raw}
{else/}
<span class="layui-text-em">暂无内容</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">调用次数</div>
<div class="detail-field-value">{$row.use_num|default=0}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">最后使用时间</div>
<div class="detail-field-value">
{if condition="$row.last_use_time && $row.last_use_time > 0"}
{:date('Y-m-d H:i:s', $row.last_use_time)}
{else/}
<span class="layui-text-em">未使用</span>
{/if}
</div>
</div>
</div>
</div>
<!-- 右侧基础信息 -->
<div class="layui-col-md4 detail-side">
<h3 class="detail-side-title">基础信息</h3>
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">ID</div>
<div class="detail-field-value">{$row.id}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">状态</div>
<div class="detail-field-value">
<span class="layui-badge">{$select_list_status[$row.status]|default=''}</span>
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">创建时间</div>
<div class="detail-field-value">
{if condition="$row.create_time && $row.create_time > 0"}
{:date('Y-m-d H:i:s', $row.create_time)}
{else/}
<span class="layui-text-em">暂无数据</span>
{/if}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
$(function(){
// 删除数据
window.deleteData = function(id) {
layer.confirm('确定要删除这条数据吗?', {
icon: 3,
title: '提示'
}, function(index) {
$.post('{{:url("delete")}}', {id: id}, function(res) {
if (res.code == 0) {
layer.msg('删除成功', {icon: 1}, function() {
location.href = '{{:url("index")}}';
});
} else {
layer.msg(res.msg, {icon: 2});
}
}, 'json');
layer.close(index);
});
};
ua.listen();
})