This commit is contained in:
@@ -492,6 +492,55 @@ body {
|
||||
|
||||
.modal-wide .modal-body {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
/* 访问记录:过滤栏与分页 */
|
||||
.audit-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.audit-filters input,
|
||||
.audit-filters select {
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid #adadad;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.audit-filters #audit-filter-key {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.audit-filters input[type="datetime-local"] {
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.audit-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.audit-pagination .btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.audit-pagination select {
|
||||
height: 26px;
|
||||
border: 1px solid #adadad;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
||||
+26
-1
@@ -311,7 +311,22 @@
|
||||
<button class="modal-close" onclick="closeModal('modal-audit')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="overflow:auto;max-height:480px">
|
||||
<div class="audit-filters">
|
||||
<input type="text" id="audit-filter-key" placeholder="键名(模糊匹配)">
|
||||
<select id="audit-filter-action">
|
||||
<option value="">全部操作</option>
|
||||
<option value="read">读取</option>
|
||||
<option value="create">新增</option>
|
||||
<option value="update">更新</option>
|
||||
<option value="delete">删除</option>
|
||||
</select>
|
||||
<input type="datetime-local" id="audit-filter-start">
|
||||
<span style="color:#999">至</span>
|
||||
<input type="datetime-local" id="audit-filter-end">
|
||||
<button class="btn btn-primary" id="btn-audit-query">查询</button>
|
||||
<button class="btn btn-secondary" id="btn-audit-reset">重置</button>
|
||||
</div>
|
||||
<div style="overflow:auto;max-height:52vh">
|
||||
<table class="config-table" id="audit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -321,6 +336,16 @@
|
||||
<tbody id="audit-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="audit-pagination">
|
||||
<button class="btn btn-secondary" id="btn-audit-prev">上一页</button>
|
||||
<span id="audit-page-info"></span>
|
||||
<button class="btn btn-secondary" id="btn-audit-next">下一页</button>
|
||||
<select id="audit-page-size" style="width:auto">
|
||||
<option value="20">20 条/页</option>
|
||||
<option value="50">50 条/页</option>
|
||||
<option value="100">100 条/页</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('modal-audit')">关闭</button>
|
||||
|
||||
+9
-3
@@ -93,9 +93,15 @@ const API = {
|
||||
return this.request(`/api/projects/${projectId}/configs/${configId}`, { method: "DELETE" });
|
||||
},
|
||||
|
||||
getAudit(projectId, key) {
|
||||
const qs = key ? `?key=${encodeURIComponent(key)}` : "";
|
||||
return this.request(`/api/projects/${projectId}/configs/audit${qs}`);
|
||||
getAudit(projectId, filters = {}) {
|
||||
const qs = new URLSearchParams();
|
||||
for (const k of ["key", "action", "start", "end", "page", "pageSize"]) {
|
||||
if (filters[k] !== undefined && filters[k] !== null && filters[k] !== "") {
|
||||
qs.append(k, filters[k]);
|
||||
}
|
||||
}
|
||||
const str = qs.toString();
|
||||
return this.request(`/api/projects/${projectId}/configs/audit${str ? "?" + str : ""}`);
|
||||
},
|
||||
|
||||
// ========== 模式 ==========
|
||||
|
||||
+108
-13
@@ -671,9 +671,22 @@ async function copyAddress() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问记录查询状态
|
||||
*/
|
||||
const auditState = {
|
||||
key: "",
|
||||
action: "",
|
||||
start: "",
|
||||
end: "",
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开访问记录弹窗
|
||||
* @param {string|null} key - 为空时查当前项目全部记录;否则查指定配置键的记录
|
||||
* @param {string|null} key - 传入配置键时作为过滤条件预填;为空时按当前过滤条件查全部记录
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function openAudit(key) {
|
||||
@@ -681,32 +694,80 @@ async function openAudit(key) {
|
||||
toast("请先选择项目", "error");
|
||||
return;
|
||||
}
|
||||
document.getElementById("audit-title").textContent = key
|
||||
? `(${state.currentProject.name} · ${key})`
|
||||
: `(${state.currentProject.name})`;
|
||||
document.getElementById("audit-title").textContent = `(${state.currentProject.name})`;
|
||||
if (key) {
|
||||
auditState.key = key;
|
||||
document.getElementById("audit-filter-key").value = key;
|
||||
}
|
||||
openModal("modal-audit");
|
||||
await loadAudit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取当前过滤条件并分页加载访问记录
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function loadAudit() {
|
||||
if (!state.currentProject) return;
|
||||
auditState.key = document.getElementById("audit-filter-key").value.trim();
|
||||
auditState.action = document.getElementById("audit-filter-action").value;
|
||||
auditState.start = toUtcIso(document.getElementById("audit-filter-start").value);
|
||||
auditState.end = toUtcIso(document.getElementById("audit-filter-end").value);
|
||||
auditState.pageSize = parseInt(document.getElementById("audit-page-size").value, 10) || 20;
|
||||
try {
|
||||
const logs = await API.getAudit(state.currentProject.id, key || "");
|
||||
renderAudit(logs);
|
||||
const data = await API.getAudit(state.currentProject.id, {
|
||||
key: auditState.key,
|
||||
action: auditState.action,
|
||||
start: auditState.start,
|
||||
end: auditState.end,
|
||||
page: auditState.page,
|
||||
pageSize: auditState.pageSize,
|
||||
});
|
||||
auditState.total = data.total || 0;
|
||||
renderAudit(data.items || []);
|
||||
renderAuditPagination(data);
|
||||
} catch (err) {
|
||||
toast("加载访问记录失败: " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染访问记录表格
|
||||
* @param {Array} logs - 访问记录列表
|
||||
* datetime-local 输入值转 UTC ISO 字符串
|
||||
* @param {string} value - 形如 "2026-09-21T12:30"(浏览器本地时区)
|
||||
* @returns {string} UTC ISO,空值原样返回
|
||||
*/
|
||||
function toUtcIso(value) {
|
||||
if (!value) return "";
|
||||
const d = new Date(value);
|
||||
return isNaN(d.getTime()) ? "" : d.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染分页信息
|
||||
* @param {Object} data - { total, page, pageSize }
|
||||
* @returns {void}
|
||||
*/
|
||||
function renderAudit(logs) {
|
||||
function renderAuditPagination(data) {
|
||||
const totalPages = Math.max(1, Math.ceil((data.total || 0) / (data.pageSize || 1)));
|
||||
document.getElementById("btn-audit-prev").disabled = data.page <= 1;
|
||||
document.getElementById("btn-audit-next").disabled = data.page >= totalPages;
|
||||
document.getElementById("audit-page-info").textContent = `第 ${data.page} / ${totalPages} 页 · 共 ${data.total} 条`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染访问记录表格
|
||||
* @param {Array} items - 当前页访问记录列表
|
||||
* @returns {void}
|
||||
*/
|
||||
function renderAudit(items) {
|
||||
const tbody = document.getElementById("audit-tbody");
|
||||
if (!logs || !logs.length) {
|
||||
if (!items || !items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;padding:24px">暂无访问记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
const ACTION_NAMES = { read: "读取", create: "新增", update: "更新", delete: "删除" };
|
||||
const SOURCE_NAMES = { web: "网页", mcp: "MCP" };
|
||||
tbody.innerHTML = logs
|
||||
tbody.innerHTML = items
|
||||
.map((l) => {
|
||||
const act = ACTION_NAMES[l.action] || l.action;
|
||||
const src = SOURCE_NAMES[l.source] || l.source;
|
||||
@@ -867,8 +928,42 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
// 操作记录
|
||||
document.getElementById("btn-audit").addEventListener("click", () => openAudit(null));
|
||||
// 访问记录:打开弹窗
|
||||
document.getElementById("btn-audit").addEventListener("click", () => openAudit(null));
|
||||
|
||||
// 访问记录:过滤与分页
|
||||
document.getElementById("btn-audit-query").addEventListener("click", () => {
|
||||
auditState.page = 1;
|
||||
loadAudit();
|
||||
});
|
||||
document.getElementById("btn-audit-reset").addEventListener("click", () => {
|
||||
document.getElementById("audit-filter-key").value = "";
|
||||
document.getElementById("audit-filter-action").value = "";
|
||||
document.getElementById("audit-filter-start").value = "";
|
||||
document.getElementById("audit-filter-end").value = "";
|
||||
auditState.page = 1;
|
||||
loadAudit();
|
||||
});
|
||||
document.getElementById("btn-audit-prev").addEventListener("click", () => {
|
||||
if (auditState.page > 1) {
|
||||
auditState.page -= 1;
|
||||
loadAudit();
|
||||
}
|
||||
});
|
||||
document.getElementById("btn-audit-next").addEventListener("click", () => {
|
||||
auditState.page += 1;
|
||||
loadAudit();
|
||||
});
|
||||
document.getElementById("audit-page-size").addEventListener("change", () => {
|
||||
auditState.page = 1;
|
||||
loadAudit();
|
||||
});
|
||||
document.getElementById("audit-filter-key").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
auditState.page = 1;
|
||||
loadAudit();
|
||||
}
|
||||
});
|
||||
|
||||
// 表格操作(事件委托)
|
||||
document.getElementById("config-tbody").addEventListener("click", (e) => {
|
||||
|
||||
Reference in New Issue
Block a user