feat: add delete options for proxy nodes and history records
This commit is contained in:
+50
-5
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user