feat(ui): working bulk operations on servers/customers/nodes lists (item 24)
This commit is contained in:
+121
-8
@@ -9,7 +9,9 @@ const state = {
|
|||||||
protocols: [], realityPresets: [], version: '0.6.8',
|
protocols: [], realityPresets: [], version: '0.6.8',
|
||||||
search: '', groupFilter: '', customerNodeFilter: '',
|
search: '', groupFilter: '', customerNodeFilter: '',
|
||||||
pages: { servers: 1, customers: 1, nodes: 1 },
|
pages: { servers: 1, customers: 1, nodes: 1 },
|
||||||
addrVisible: false, expandedServers: new Set()
|
addrVisible: false, expandedServers: new Set(),
|
||||||
|
selection: { servers: new Set(), customers: new Set(), nodes: new Set() },
|
||||||
|
bulkRunning: false
|
||||||
};
|
};
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@@ -471,6 +473,102 @@ function serverDetailRow(server) {
|
|||||||
<button class="btn small" data-action="agent-uninstall" data-id="${esc(server.id)}">SSH 卸载 Agent</button>
|
<button class="btn small" data-action="agent-uninstall" data-id="${esc(server.id)}">SSH 卸载 Agent</button>
|
||||||
</div></td></tr>`;
|
</div></td></tr>`;
|
||||||
}
|
}
|
||||||
|
/* ---------- 列表批量操作(第 24 条) ----------
|
||||||
|
每页一份选中集合(state.selection),行勾选框与表头全选维护它;
|
||||||
|
选中 ≥1 时工具条下出现批量条。批量动作逐项调用既有单项接口,
|
||||||
|
语义与守卫与单项完全一致:服务器 启停=PATCH enabled / 删除=DELETE
|
||||||
|
(仍被节点引用等守卫由服务端拒绝);客户 启用=status active /
|
||||||
|
停用=status suspended(与行内开关的 manual 口径一致)/ 删除=
|
||||||
|
DELETE;入站 启用=deploy / 停用=remove / 删除=DELETE。
|
||||||
|
选择状态口径:按页独立保存,重渲染与 45 秒自动刷新后按集合恢复
|
||||||
|
勾选;翻页/筛选不清空(全选只作用于当前渲染的行);批量执行后
|
||||||
|
清空本页集合。 */
|
||||||
|
const BULK_DEFS = {
|
||||||
|
servers: {
|
||||||
|
get: (id) => serverById(id),
|
||||||
|
label: (item) => item.name,
|
||||||
|
isOn: (item) => item.enabled !== false,
|
||||||
|
enable: (item) => api(`/api/servers/${item.id}`, { method: 'PATCH', body: JSON.stringify({ enabled: true }) }),
|
||||||
|
disable: (item) => api(`/api/servers/${item.id}`, { method: 'PATCH', body: JSON.stringify({ enabled: false }) }),
|
||||||
|
remove: (item) => api(`/api/servers/${item.id}`, { method: 'DELETE' }),
|
||||||
|
deleteConfirm: (n) => `删除选中的 ${n} 台设备?将删除控制台登记并吊销其 Agent 密钥,此操作不可恢复。`,
|
||||||
|
page: 'servers'
|
||||||
|
},
|
||||||
|
customers: {
|
||||||
|
get: (id) => customerById(id),
|
||||||
|
label: (item) => item.name,
|
||||||
|
isOn: (item) => !(item.status === 'suspended' && item.suspendReason === 'manual'),
|
||||||
|
enable: (item) => api(`/api/customers/${item.id}`, { method: 'PATCH', body: JSON.stringify({ status: 'active' }) }),
|
||||||
|
disable: (item) => api(`/api/customers/${item.id}`, { method: 'PATCH', body: JSON.stringify({ status: 'suspended' }) }),
|
||||||
|
remove: (item) => api(`/api/customers/${item.id}`, { method: 'DELETE' }),
|
||||||
|
deleteConfirm: (n) => `删除选中的 ${n} 位客户及订阅地址?此操作不可恢复(仍有节点分配的客户会被逐项拒绝)。`,
|
||||||
|
page: 'customers'
|
||||||
|
},
|
||||||
|
nodes: {
|
||||||
|
get: (id) => nodeById(id),
|
||||||
|
label: (item) => item.name,
|
||||||
|
isOn: (item) => !['draft', 'removing'].includes(item.status),
|
||||||
|
busy: (item) => ['deploying', 'removing'].includes(item.status),
|
||||||
|
enable: (item) => api(`/api/nodes/${item.id}/deploy`, { method: 'POST', body: '{}' }),
|
||||||
|
disable: (item) => api(`/api/nodes/${item.id}/remove`, { method: 'POST', body: '{}' }),
|
||||||
|
remove: (item) => api(`/api/nodes/${item.id}`, { method: 'DELETE' }),
|
||||||
|
deleteConfirm: (n) => `删除选中的 ${n} 个入站?会移除全部分配并从设备清理资源,此操作不可恢复。`,
|
||||||
|
page: 'chains'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function bulkBarHTML(key) {
|
||||||
|
const n = state.selection[key].size;
|
||||||
|
if (!n) return '';
|
||||||
|
return `<div class="bulk-bar"><span class="bulk-count">已选 <b>${n}</b> 项</span>
|
||||||
|
<button class="btn small" data-action="bulk-enable" data-bulk-page="${key}">启用</button>
|
||||||
|
<button class="btn small" data-action="bulk-disable" data-bulk-page="${key}">停用</button>
|
||||||
|
<button class="btn small danger" data-action="bulk-delete" data-bulk-page="${key}">删除</button>
|
||||||
|
<button class="btn small" data-action="bulk-clear" data-bulk-page="${key}">取消选择</button></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBulk(key, op) {
|
||||||
|
if (state.bulkRunning) return;
|
||||||
|
const def = BULK_DEFS[key]; if (!def) return;
|
||||||
|
const ids = [...state.selection[key]];
|
||||||
|
if (!ids.length) return;
|
||||||
|
if (op === 'delete' && !await confirmModal(def.deleteConfirm(ids.length), '删除')) return;
|
||||||
|
state.bulkRunning = true;
|
||||||
|
const done = []; const failed = []; const skipped = [];
|
||||||
|
try {
|
||||||
|
for (const id of ids) {
|
||||||
|
const item = def.get(id);
|
||||||
|
if (!item) { skipped.push('已不存在'); continue; }
|
||||||
|
const name = def.label(item) || id;
|
||||||
|
if (op !== 'delete') {
|
||||||
|
if (def.busy && def.busy(item)) { skipped.push(`${name}:正忙`); continue; }
|
||||||
|
if ((op === 'enable') === def.isOn(item)) { skipped.push(`${name}:已是${op === 'enable' ? '启用' : '停用'}状态`); continue; }
|
||||||
|
}
|
||||||
|
try { await def[op === 'delete' ? 'remove' : op](item); done.push(name); }
|
||||||
|
catch (error) { failed.push(`${name}:${error.message}`); }
|
||||||
|
}
|
||||||
|
} finally { state.bulkRunning = false; }
|
||||||
|
state.selection[key].clear();
|
||||||
|
const verb = op === 'delete' ? '删除' : op === 'enable' ? '启用' : '停用';
|
||||||
|
const brief = (list) => list.slice(0, 3).join(';') + (list.length > 3 ? `;等 ${list.length} 项` : '');
|
||||||
|
let msg = `批量${verb}:成功 ${done.length} 项`;
|
||||||
|
if (skipped.length) msg += `,跳过 ${skipped.length} 项(${brief(skipped)})`;
|
||||||
|
if (failed.length) msg += `,失败 ${failed.length} 项(${brief(failed)})`;
|
||||||
|
toast(msg, failed.length > 0);
|
||||||
|
await load(def.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* render 后同步表头全选框的三态(全选/半选/未选)。 */
|
||||||
|
function syncCheckAll() {
|
||||||
|
const all = $('[data-check-all]');
|
||||||
|
if (!all) return;
|
||||||
|
const boxes = $$('[data-row-check]');
|
||||||
|
if (!boxes.length) { all.checked = false; all.indeterminate = false; return; }
|
||||||
|
const n = boxes.filter((box) => box.checked).length;
|
||||||
|
all.checked = n === boxes.length;
|
||||||
|
all.indeterminate = n > 0 && n < boxes.length;
|
||||||
|
}
|
||||||
|
|
||||||
function renderServers() {
|
function renderServers() {
|
||||||
const q = state.search.toLowerCase();
|
const q = state.search.toLowerCase();
|
||||||
const onlineCount = state.servers.filter((s) => s.status === 'online').length;
|
const onlineCount = state.servers.filter((s) => s.status === 'online').length;
|
||||||
@@ -490,7 +588,7 @@ function renderServers() {
|
|||||||
const addr = state.addrVisible ? esc(server.publicAddress) : esc(maskAddr(server.publicAddress));
|
const addr = state.addrVisible ? esc(server.publicAddress) : esc(maskAddr(server.publicAddress));
|
||||||
const row = `<tr>
|
const row = `<tr>
|
||||||
<td><button class="exp" type="button" data-action="expand-server" data-id="${esc(server.id)}">${expanded ? '−' : '+'}</button></td>
|
<td><button class="exp" type="button" data-action="expand-server" data-id="${esc(server.id)}">${expanded ? '−' : '+'}</button></td>
|
||||||
<td><input type="checkbox" data-row-check></td>
|
<td><input type="checkbox" data-row-check="servers" data-id="${esc(server.id)}"${state.selection.servers.has(server.id) ? ' checked' : ''}></td>
|
||||||
<td><div class="op-cell"><button class="icon-btn" data-tip="编辑" data-action="edit-server" data-id="${esc(server.id)}">${ICONS.edit}</button><button class="icon-btn" data-tip="复制注册命令" data-action="enroll-server" data-id="${esc(server.id)}">${ICONS.copy}</button><button class="icon-btn del" data-tip="删除" data-action="delete-server" data-id="${esc(server.id)}">${ICONS.trash}</button></div></td>
|
<td><div class="op-cell"><button class="icon-btn" data-tip="编辑" data-action="edit-server" data-id="${esc(server.id)}">${ICONS.edit}</button><button class="icon-btn" data-tip="复制注册命令" data-action="enroll-server" data-id="${esc(server.id)}">${ICONS.copy}</button><button class="icon-btn del" data-tip="删除" data-action="delete-server" data-id="${esc(server.id)}">${ICONS.trash}</button></div></td>
|
||||||
<td><label class="tgl"><input type="checkbox" data-toggle-server data-id="${esc(server.id)}" ${server.enabled !== false ? 'checked' : ''}><span class="tr"></span></label></td>
|
<td><label class="tgl"><input type="checkbox" data-toggle-server data-id="${esc(server.id)}" ${server.enabled !== false ? 'checked' : ''}><span class="tr"></span></label></td>
|
||||||
<td><span class="dev-name">${esc(server.name)}</span></td>
|
<td><span class="dev-name">${esc(server.name)}</span></td>
|
||||||
@@ -507,7 +605,7 @@ function renderServers() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
return `${stats}
|
return `${stats}
|
||||||
<div class="page-tools"><button class="btn primary" data-action="add-server">+ 添加设备</button><input class="search" data-search placeholder="搜索设备名、地址" value="${esc(state.search)}"></div>
|
<div class="page-tools"><button class="btn primary" data-action="add-server">+ 添加设备</button><input class="search" data-search placeholder="搜索设备名、地址" value="${esc(state.search)}"></div>
|
||||||
<section class="panel"><div class="table-wrap"><table class="w-servers"><thead><tr><th></th><th><input type="checkbox" data-check-all></th><th>操作</th><th>已启用</th><th>名称</th><th>地址<button class="icon-btn eye-btn" type="button" data-action="toggle-addr" data-tip="显示 / 隐藏地址">${state.addrVisible ? ICONS.eyeOff : ICONS.eye}</button></th><th>状态</th><th>CPU</th><th>内存</th><th>Agent 版本</th><th>核心版本</th><th>运行时长</th><th>客户端</th><th>上次心跳</th></tr></thead>
|
${bulkBarHTML('servers')}<section class="panel"><div class="table-wrap"><table class="w-servers"><thead><tr><th></th><th><input type="checkbox" data-check-all="servers"></th><th>操作</th><th>已启用</th><th>名称</th><th>地址<button class="icon-btn eye-btn" type="button" data-action="toggle-addr" data-tip="显示 / 隐藏地址">${state.addrVisible ? ICONS.eyeOff : ICONS.eye}</button></th><th>状态</th><th>CPU</th><th>内存</th><th>Agent 版本</th><th>核心版本</th><th>运行时长</th><th>客户端</th><th>上次心跳</th></tr></thead>
|
||||||
<tbody>${rows || emptyRow(14, '没有匹配设备')}</tbody></table></div>${pagerHTML('servers', filtered.length)}</section>`;
|
<tbody>${rows || emptyRow(14, '没有匹配设备')}</tbody></table></div>${pagerHTML('servers', filtered.length)}</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +663,7 @@ function renderCustomers() {
|
|||||||
const speedTotal = (item.speedUpBps || 0) + (item.speedDownBps || 0);
|
const speedTotal = (item.speedUpBps || 0) + (item.speedDownBps || 0);
|
||||||
const manualOff = item.status === 'suspended' && item.suspendReason === 'manual';
|
const manualOff = item.status === 'suspended' && item.suspendReason === 'manual';
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><div class="op-cell"><input type="checkbox" data-row-check><button class="icon-btn" data-tip="二维码" data-action="customer-subscription" data-id="${esc(item.id)}">${ICONS.qr}</button><button class="icon-btn" data-tip="详情" data-action="customer-access" data-id="${esc(item.id)}">${ICONS.info}</button><button class="icon-btn" data-tip="重置流量" data-action="reset-usage" data-id="${esc(item.id)}">${ICONS.reset}</button><button class="icon-btn" data-tip="编辑" data-action="edit-customer" data-id="${esc(item.id)}">${ICONS.edit}</button><button class="icon-btn del" data-tip="删除" data-action="delete-customer" data-id="${esc(item.id)}">${ICONS.trash}</button></div></td>
|
<td><div class="op-cell"><input type="checkbox" data-row-check="customers" data-id="${esc(item.id)}"${state.selection.customers.has(item.id) ? ' checked' : ''}><button class="icon-btn" data-tip="二维码" data-action="customer-subscription" data-id="${esc(item.id)}">${ICONS.qr}</button><button class="icon-btn" data-tip="详情" data-action="customer-access" data-id="${esc(item.id)}">${ICONS.info}</button><button class="icon-btn" data-tip="重置流量" data-action="reset-usage" data-id="${esc(item.id)}">${ICONS.reset}</button><button class="icon-btn" data-tip="编辑" data-action="edit-customer" data-id="${esc(item.id)}">${ICONS.edit}</button><button class="icon-btn del" data-tip="删除" data-action="delete-customer" data-id="${esc(item.id)}">${ICONS.trash}</button></div></td>
|
||||||
<td><label class="tgl"><input type="checkbox" data-toggle-customer data-id="${esc(item.id)}" ${manualOff ? '' : 'checked'}><span class="tr"></span></label></td>
|
<td><label class="tgl"><input type="checkbox" data-toggle-customer data-id="${esc(item.id)}" ${manualOff ? '' : 'checked'}><span class="tr"></span></label></td>
|
||||||
<td><span class="pres ${item.online ? 'on' : 'off'}"><span class="dot ${item.online ? 'on' : 'off'}"></span>${item.online ? '在线' : '离线'}</span>${item.status === 'suspended' ? `<span class="paused-note">已暂停 · ${esc(suspendReasonText[item.suspendReason] || '待恢复')}</span>` : ''}</td>
|
<td><span class="pres ${item.online ? 'on' : 'off'}"><span class="dot ${item.online ? 'on' : 'off'}"></span>${item.online ? '在线' : '离线'}</span>${item.status === 'suspended' ? `<span class="paused-note">已暂停 · ${esc(suspendReasonText[item.suspendReason] || '待恢复')}</span>` : ''}</td>
|
||||||
<td><span class="cust-name">${esc(item.name)}</span>${item.notes ? `<span class="cust-note">${esc(item.notes)}</span>` : '<span class="cust-note"></span>'}</td>
|
<td><span class="cust-name">${esc(item.name)}</span>${item.notes ? `<span class="cust-note">${esc(item.notes)}</span>` : '<span class="cust-note"></span>'}</td>
|
||||||
@@ -590,7 +688,7 @@ function renderCustomers() {
|
|||||||
<select class="search" data-group-filter><option value="">全部分组</option>${groups.map((g) => `<option value="${esc(g)}"${selected(g, state.groupFilter)}>${esc(g)}</option>`).join('')}</select>
|
<select class="search" data-group-filter><option value="">全部分组</option>${groups.map((g) => `<option value="${esc(g)}"${selected(g, state.groupFilter)}>${esc(g)}</option>`).join('')}</select>
|
||||||
${filterNode ? `<span class="filter-chip">节点:${esc(filterNode.name)}<button type="button" data-action="clear-node-filter" data-tip="清除">×</button></span>` : ''}
|
${filterNode ? `<span class="filter-chip">节点:${esc(filterNode.name)}<button type="button" data-action="clear-node-filter" data-tip="清除">×</button></span>` : ''}
|
||||||
<span class="badge blue">${filtered.length} 位客户</span></div></div>
|
<span class="badge blue">${filtered.length} 位客户</span></div></div>
|
||||||
<section class="panel"><div class="table-wrap"><table class="w-customers"><thead><tr><th>操作</th><th>已启用</th><th>在线</th><th>客户端</th><th>关联入站</th><th>流量<span class="sarr">⇅</span></th><th class="ctr">速度</th><th>剩余<span class="sarr">⇅</span></th><th>时长<span class="sarr">⇅</span></th></tr></thead>
|
${bulkBarHTML('customers')}<section class="panel"><div class="table-wrap"><table class="w-customers"><thead><tr><th><input type="checkbox" data-check-all="customers"> 操作</th><th>已启用</th><th>在线</th><th>客户端</th><th>关联入站</th><th>流量<span class="sarr">⇅</span></th><th class="ctr">速度</th><th>剩余<span class="sarr">⇅</span></th><th>时长<span class="sarr">⇅</span></th></tr></thead>
|
||||||
<tbody>${rows || emptyRow(9, '还没有客户')}</tbody></table></div>${pagerHTML('customers', filtered.length)}</section>`;
|
<tbody>${rows || emptyRow(9, '还没有客户')}</tbody></table></div>${pagerHTML('customers', filtered.length)}</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,7 +1000,7 @@ function renderNodes() {
|
|||||||
const busy = ['deploying', 'removing'].includes(node.status);
|
const busy = ['deploying', 'removing'].includes(node.status);
|
||||||
return `<tr data-node-row="${esc(node.id)}">
|
return `<tr data-node-row="${esc(node.id)}">
|
||||||
<td><span class="grip" data-tip="拖动排序" data-grip>${ICONS.grip}</span></td>
|
<td><span class="grip" data-tip="拖动排序" data-grip>${ICONS.grip}</span></td>
|
||||||
<td><input type="checkbox" data-row-check></td>
|
<td><input type="checkbox" data-row-check="nodes" data-id="${esc(node.id)}"${state.selection.nodes.has(node.id) ? ' checked' : ''}></td>
|
||||||
<td class="num">${(node.sortIndex != null ? node.sortIndex : index) + 1}</td>
|
<td class="num">${(node.sortIndex != null ? node.sortIndex : index) + 1}</td>
|
||||||
<td class="menu-td"><button class="icon-btn" data-tip="编辑" data-action="edit-node" data-id="${esc(node.id)}">${ICONS.edit}</button><button class="icon-btn" data-tip="更多" data-action="node-menu" data-id="${esc(node.id)}">${ICONS.dots}</button></td>
|
<td class="menu-td"><button class="icon-btn" data-tip="编辑" data-action="edit-node" data-id="${esc(node.id)}">${ICONS.edit}</button><button class="icon-btn" data-tip="更多" data-action="node-menu" data-id="${esc(node.id)}">${ICONS.dots}</button></td>
|
||||||
<td><label class="tgl"><input type="checkbox" data-toggle-node data-id="${esc(node.id)}" ${enabled ? 'checked' : ''} ${busy ? 'disabled' : ''}><span class="tr"></span></label></td>
|
<td><label class="tgl"><input type="checkbox" data-toggle-node data-id="${esc(node.id)}" ${enabled ? 'checked' : ''} ${busy ? 'disabled' : ''}><span class="tr"></span></label></td>
|
||||||
@@ -918,7 +1016,7 @@ function renderNodes() {
|
|||||||
return `<div class="page-tools"><button class="btn primary" data-action="add-node">+ 添加入站</button>
|
return `<div class="page-tools"><button class="btn primary" data-action="add-node">+ 添加入站</button>
|
||||||
<div class="toolbar"><input class="search" data-search placeholder="搜索节点、服务器或协议" value="${esc(state.search)}">
|
<div class="toolbar"><input class="search" data-search placeholder="搜索节点、服务器或协议" value="${esc(state.search)}">
|
||||||
<span class="badge blue">${state.nodes.length} 个入站</span><span class="badge green">在线 ${active}</span>${degraded ? `<span class="badge orange">降级 ${degraded}</span>` : ''}</div></div>
|
<span class="badge blue">${state.nodes.length} 个入站</span><span class="badge green">在线 ${active}</span>${degraded ? `<span class="badge orange">降级 ${degraded}</span>` : ''}</div></div>
|
||||||
<section class="panel"><div class="table-wrap visible"><table class="w-nodes"><thead><tr><th></th><th><input type="checkbox" data-check-all></th><th>ID<span class="sarr">⇅</span></th><th>菜单</th><th>启用</th><th>备注</th><th>节点</th><th>端口<span class="sarr">⇅</span></th><th>协议</th><th>客户端</th><th>流量<span class="sarr">⇅</span></th><th>速度</th><th>到期时间</th></tr></thead>
|
${bulkBarHTML('nodes')}<section class="panel"><div class="table-wrap visible"><table class="w-nodes"><thead><tr><th></th><th><input type="checkbox" data-check-all="nodes"></th><th>ID<span class="sarr">⇅</span></th><th>菜单</th><th>启用</th><th>备注</th><th>节点</th><th>端口<span class="sarr">⇅</span></th><th>协议</th><th>客户端</th><th>流量<span class="sarr">⇅</span></th><th>速度</th><th>到期时间</th></tr></thead>
|
||||||
<tbody>${rows || emptyRow(13, '还没有节点')}</tbody></table></div>${pagerHTML('nodes', filtered.length)}</section>`;
|
<tbody>${rows || emptyRow(13, '还没有节点')}</tbody></table></div>${pagerHTML('nodes', filtered.length)}</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1158,6 +1256,7 @@ function renderOperations() {
|
|||||||
function render() {
|
function render() {
|
||||||
const views = { overview: renderOverview, servers: renderServers, customers: renderCustomers, chains: renderNodes, operations: renderOperations };
|
const views = { overview: renderOverview, servers: renderServers, customers: renderCustomers, chains: renderNodes, operations: renderOperations };
|
||||||
$('#content').innerHTML = views[state.page]();
|
$('#content').innerHTML = views[state.page]();
|
||||||
|
syncCheckAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 表单提交 ---------- */
|
/* ---------- 表单提交 ---------- */
|
||||||
@@ -1384,6 +1483,10 @@ document.addEventListener('click', async (event) => {
|
|||||||
else if (action === 'repair-node') { hidePop(); if (!await confirmModal('重新下发该节点配置?', '修复')) return; await api(`/api/nodes/${itemId}/repair`, { method: 'POST', body: '{}' }); toast('修复任务已进入队列'); await load(); }
|
else if (action === 'repair-node') { hidePop(); if (!await confirmModal('重新下发该节点配置?', '修复')) return; await api(`/api/nodes/${itemId}/repair`, { method: 'POST', body: '{}' }); toast('修复任务已进入队列'); await load(); }
|
||||||
else if (action === 'remove-node') { hidePop(); if (!await confirmModal('停用会从相关设备移除配置,节点保留为草稿,确认继续?', '停用')) return; await api(`/api/nodes/${itemId}/remove`, { method: 'POST', body: '{}' }); toast('停用任务已进入队列'); await load(); }
|
else if (action === 'remove-node') { hidePop(); if (!await confirmModal('停用会从相关设备移除配置,节点保留为草稿,确认继续?', '停用')) return; await api(`/api/nodes/${itemId}/remove`, { method: 'POST', body: '{}' }); toast('停用任务已进入队列'); await load(); }
|
||||||
else if (action === 'delete-node') { hidePop(); if (!await confirmModal('确定删除这个节点?会移除全部分配并从设备清理资源。', '删除')) return; await api(`/api/nodes/${itemId}`, { method: 'DELETE' }); toast('节点已删除,设备资源待 Agent 确认清理'); await load(); }
|
else if (action === 'delete-node') { hidePop(); if (!await confirmModal('确定删除这个节点?会移除全部分配并从设备清理资源。', '删除')) return; await api(`/api/nodes/${itemId}`, { method: 'DELETE' }); toast('节点已删除,设备资源待 Agent 确认清理'); await load(); }
|
||||||
|
else if (action === 'bulk-clear') { state.selection[button.dataset.bulkPage].clear(); render(); }
|
||||||
|
else if (action === 'bulk-enable') await runBulk(button.dataset.bulkPage, 'enable');
|
||||||
|
else if (action === 'bulk-disable') await runBulk(button.dataset.bulkPage, 'disable');
|
||||||
|
else if (action === 'bulk-delete') await runBulk(button.dataset.bulkPage, 'delete');
|
||||||
else if (action === 'download-backup') location.href = '/api/backup';
|
else if (action === 'download-backup') location.href = '/api/backup';
|
||||||
else if (action === 'restore-backup') {
|
else if (action === 'restore-backup') {
|
||||||
const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json';
|
const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json';
|
||||||
@@ -1423,8 +1526,18 @@ document.addEventListener('change', async (event) => {
|
|||||||
await load('chains');
|
await load('chains');
|
||||||
} else if (target.matches('[data-group-filter]')) {
|
} else if (target.matches('[data-group-filter]')) {
|
||||||
state.groupFilter = target.value; state.pages.customers = 1; render();
|
state.groupFilter = target.value; state.pages.customers = 1; render();
|
||||||
|
} else if (target.matches('[data-row-check]')) {
|
||||||
|
const set = state.selection[target.dataset.rowCheck];
|
||||||
|
if (set) {
|
||||||
|
if (target.checked) set.add(target.dataset.id); else set.delete(target.dataset.id);
|
||||||
|
render();
|
||||||
|
}
|
||||||
} else if (target.matches('[data-check-all]')) {
|
} else if (target.matches('[data-check-all]')) {
|
||||||
$$('[data-row-check]').forEach((box) => { box.checked = target.checked; });
|
const set = state.selection[target.dataset.checkAll];
|
||||||
|
if (set) {
|
||||||
|
$$('[data-row-check]').forEach((box) => { if (target.checked) set.add(box.dataset.id); else set.delete(box.dataset.id); });
|
||||||
|
render();
|
||||||
|
}
|
||||||
} else if (target.matches('[data-node-sync]')) syncNodeForm();
|
} else if (target.matches('[data-node-sync]')) syncNodeForm();
|
||||||
else if (target.matches('[data-exit-link]')) applyExitLink(target);
|
else if (target.matches('[data-exit-link]')) applyExitLink(target);
|
||||||
else if (target.matches('[data-reality-preset]')) {
|
else if (target.matches('[data-reality-preset]')) {
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ input,select,textarea{font-family:var(--font);color:var(--text)}
|
|||||||
.panel-body{padding:14px 16px}
|
.panel-body{padding:14px 16px}
|
||||||
.toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
.toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||||
.page-tools{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}
|
.page-tools{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}
|
||||||
|
.bulk-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px;padding:8px 12px;border:1px solid var(--primary);border-radius:8px;background:var(--card-hover)}
|
||||||
|
.bulk-bar .bulk-count{font-size:13px;color:var(--text)}
|
||||||
.search{background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:6px 12px;font-size:13px;width:260px}
|
.search{background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:6px 12px;font-size:13px;width:260px}
|
||||||
select.search{width:auto}
|
select.search{width:auto}
|
||||||
.btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--card-hover);color:var(--text);border-radius:6px;padding:5px 14px;font-size:13px;cursor:pointer;white-space:nowrap}
|
.btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--card-hover);color:var(--text);border-radius:6px;padding:5px 14px;font-size:13px;cursor:pointer;white-space:nowrap}
|
||||||
|
|||||||
Reference in New Issue
Block a user