feat: add delete options for proxy nodes and history records

This commit is contained in:
jocayn
2026-06-13 15:53:09 +08:00
parent 2c0773f4f7
commit 8f28ae3c67
2 changed files with 104 additions and 5 deletions
+50 -5
View File
@@ -765,11 +765,12 @@
<th>节点数量</th>
<th>本地下载</th>
<th>一键导入</th>
<th>操作</th>
</tr>
</thead>
<tbody id="history-list">
<tr>
<td colspan="4" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
暂无生成历史记录,运行生成配置后将在此归档
</td>
</tr>
@@ -1119,9 +1120,14 @@
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${node.server}</td>
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${node.port || node.server_port}</td>
<td>
<button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="openNodeDrawer(${idx})">
<i data-lucide="sliders" style="width: 12px; height: 12px;"></i>修改参数
</button>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="openNodeDrawer(${idx})">
<i data-lucide="sliders" style="width: 12px; height: 12px;"></i>修改参数
</button>
<button class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="deleteProxy(${idx})">
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>删除
</button>
</div>
</td>
`;
list.appendChild(tr);
@@ -1129,6 +1135,16 @@
lucide.createIcons();
}
function deleteProxy(idx) {
const nodeName = proxies[idx].name || proxies[idx].tag;
if (confirm(`确定要删除代理节点 "${nodeName}" 吗?\n(注:这仅会从当前页面内存中删除。如需应用更改,请在删除后点击右上角“保存并生成配置”按钮重新生成配置文件)`)) {
log(`在内存中删除代理节点: ${nodeName}`);
proxies.splice(idx, 1);
renderProxiesTable();
showToast('代理节点已删除');
}
}
// Modal Drawer for Editing Nodes
let currentEditNodeIndex = null;
@@ -1362,6 +1378,30 @@
}
}
async function deleteHistory(yamlFile, jsonFile, timeStr) {
if (confirm(`确定要删除该历史生成记录吗?\n生成时间: ${timeStr}\n(注意:此操作将物理删除对应的配置文件归档且不可逆!)`)) {
log(`正在请求删除历史记录: ${timeStr}`);
try {
const res = await fetch('/api/history/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ yamlFile, jsonFile })
});
if (res.ok) {
log(`成功删除历史记录: ${timeStr}`, 'success');
showToast('历史记录删除成功');
loadHistory();
} else {
const errText = await res.text();
log(`删除历史记录失败: ${errText}`, 'error');
showToast('删除历史记录失败', 'error');
}
} catch (e) {
log(`删除历史记录异常: ${e.message}`, 'error');
}
}
}
function renderHistoryList(list) {
const tbody = document.getElementById('history-list');
tbody.innerHTML = '';
@@ -1369,7 +1409,7 @@
if (!list || list.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="4" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
暂无生成历史记录,运行生成配置后将在此归档
</td>
</tr>
@@ -1411,6 +1451,11 @@
</a>
</div>
</td>
<td>
<button class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="deleteHistory('${item.yamlFile}', '${item.jsonFile}', '${item.timeStr}')">
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>删除
</button>
</td>
`;
tbody.appendChild(tr);
});
+54
View File
@@ -733,6 +733,60 @@ function startServer() {
return;
}
// API: POST /api/history/delete
if (pathname === '/api/history/delete' && req.method === 'POST') {
try {
const { yamlFile, jsonFile } = await readJsonBody(req);
if (!yamlFile || !jsonFile) {
res.writeHead(400);
res.end('Missing yamlFile or jsonFile parameter');
return;
}
// Security check: prevent directory traversal
const safeRegex = /^CatMata-sub-\d{8}-\d{6}\.(yaml|json)$/;
if (!safeRegex.test(yamlFile) || !safeRegex.test(jsonFile)) {
res.writeHead(400);
res.end('Invalid file name');
return;
}
const historyDir = path.join(outDir, 'history');
const yamlPath = path.join(historyDir, yamlFile);
const jsonPath = path.join(historyDir, jsonFile);
// Delete physical files
if (fs.existsSync(yamlPath)) {
fs.unlinkSync(yamlPath);
}
if (fs.existsSync(jsonPath)) {
fs.unlinkSync(jsonPath);
}
// Update history.json
const historyJsonPath = path.join(historyDir, 'history.json');
let historyList = [];
if (fs.existsSync(historyJsonPath)) {
try {
historyList = JSON.parse(fs.readFileSync(historyJsonPath, 'utf-8'));
} catch (e) {
console.error("Failed to parse history.json, resetting:", e.message);
}
}
historyList = historyList.filter(item => item.yamlFile !== yamlFile || item.jsonFile !== jsonFile);
fs.writeFileSync(historyJsonPath, JSON.stringify(historyList, null, 2), 'utf-8');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
} catch (e) {
res.writeHead(500);
res.end(e.message);
}
return;
}
res.writeHead(404);
res.end('Not Found');
});