Files
wxauto_api/static/js/app.js
T
2026-04-07 14:11:45 +08:00

760 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ///
// app.js
// 描述:中控系统前端主逻辑
// 作者:AI Generated
// 创建日期:2026-04-05
// ///
let currentPage = 'dashboard';
let logPollInterval = null;
let lastLogTime = null;
document.addEventListener('DOMContentLoaded', () => {
renderApp();
});
function renderApp() {
document.getElementById('app').innerHTML = APP_TEMPLATE;
initNavEvents();
showPage('dashboard');
}
function initNavEvents() {
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
showPage(item.dataset.page);
});
});
}
function showPage(pageName) {
currentPage = pageName;
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelector(`[data-page="${pageName}"]`).classList.add('active');
const content = document.getElementById('page-content');
content.innerHTML = `<div class="page active">${PAGES[pageName] || '<p>页面不存在</p>'}</div>`;
if (pageName === 'dashboard') loadDashboard();
if (pageName === 'nodes') loadNodesForTable();
if (pageName === 'monitor') { loadMonitorConfig(); loadWebhookTable(); }
if (pageName === 'send') loadNodesForSend();
if (pageName === 'logs') startLogStream();
if (pageName === 'external') initExternalPage();
if (pageName === 'plugins') setTimeout(loadPluginTable, 50);
}
async function loadDashboard() {
const result = await api('/nodes/');
if (result.success) renderDashboard(result.data);
}
async function loadNodesForTable() {
const result = await api('/nodes/');
if (result.success) renderNodesTable(result.data);
}
function renderDashboard(nodes) {
const total = nodes.length;
const online = nodes.filter(n => n.status === 'active' && n.wechat_status === 'online').length;
const offline = nodes.filter(n => n.status !== 'active' || n.wechat_status !== 'online').length;
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
setText('onlineCount', online);
setText('offlineCount', offline);
setText('totalMessages', total);
setText('todayMessages', '-');
}
function renderNodesTable(nodes) {
const container = document.getElementById('nodesTable');
if (!container) return;
if (!nodes || !nodes.length) {
container.innerHTML = '<p class="empty">暂无节点</p>';
return;
}
container.innerHTML = `
<table class="table">
<thead>
<tr><th>ID</th><th>名称</th><th>分组</th><th>API状态</th><th>微信状态</th><th>操作</th></tr>
</thead>
<tbody>
${nodes.map(n => {
const statusClass = n.status !== 'active' ? 'offline' : (n.wechat_status !== 'online' ? 'warning' : 'online');
return `<tr>
<td>${n.node_id}</td>
<td>${n.name}</td>
<td>${n.group}</td>
<td><span class="status-${n.status}">${n.status}</span></td>
<td><span class="status-wechat-${n.wechat_status}">${n.wechat_status}</span></td>
<td>
<button class="btn btn-xs" onclick="checkNodeStatus('${n.node_id}')">检测</button>
<button class="btn btn-xs btn-secondary" onclick="editNode('${n.node_id}')">编辑</button>
<button class="btn btn-xs btn-danger" onclick="deleteNode('${n.node_id}')">删除</button>
</td>
</tr>`;
}).join('')}
</tbody>
</table>
`;
}
async function loadNodesForSend() {
const result = await api('/nodes/');
if (result.success) {
const options = '<option value="">选择节点</option>' +
result.data.map(n => `<option value="${n.node_id}">${n.name} (${n.node_id})</option>`).join('');
document.getElementById('sendNode').innerHTML = options;
}
}
async function refreshAll() {
await loadDashboard();
showToast('刷新完成');
}
async function checkNodeStatus(nodeId) {
const result = await api(`/nodes/${nodeId}/status`);
if (result.success) {
showToast(`检测完成: API=${result.data.api_status}, 微信=${result.data.wechat_status || 'N/A'}`);
}
await loadNodesForTable();
}
async function sendQuickMessage() {
const nodeEl = document.getElementById('sendNode');
const whoEl = document.getElementById('sendWho');
const msgEl = document.getElementById('quickMsg');
if (!nodeEl || !whoEl || !msgEl) {
showToast('页面未正确加载', 'error');
return;
}
const nodeId = nodeEl.value;
const who = whoEl.value;
const msg = msgEl.value;
if (!nodeId || !who || !msg) {
showToast('请填写完整信息', 'error');
return;
}
const result = await api('/messages/send', {
method: 'POST',
body: JSON.stringify({ node_id: nodeId, who, msg })
});
if (result.success) {
showToast('发送成功');
msgEl.value = '';
} else {
showToast('发送失败: ' + (result.error || result.detail), 'error');
}
}
async function loadMonitorConfig() {
const result = await api('/config/monitor');
if (result.success) {
const enabledEl = document.getElementById('monitorEnabled');
const intervalEl = document.getElementById('checkInterval');
if (enabledEl) enabledEl.checked = result.data.enabled;
if (intervalEl) intervalEl.value = result.data.check_interval;
}
}
async function updateMonitorConfig() {
const config = {
enabled: document.getElementById('monitorEnabled').checked,
check_interval: parseInt(document.getElementById('checkInterval').value) || 60
};
const result = await api('/config/monitor', { method: 'POST', body: JSON.stringify(config) });
if (result.success) showToast('监控配置已更新');
}
async function loadWebhookTable() {
const result = await api('/webhook/');
if (result.success) renderWebhookTable(result.data || []);
}
function renderWebhookTable(webhooks) {
const container = document.getElementById('webhookTable');
if (!container) return;
if (!webhooks.length) {
container.innerHTML = '<p class="empty">暂无 Webhook,请点击添加</p>';
return;
}
container.innerHTML = `
<table class="table">
<thead>
<tr><th>名称</th><th>URL</th><th>格式</th><th>事件</th><th>状态</th><th>操作</th></tr>
</thead>
<tbody>
${webhooks.map(w => {
const eventLabels = {
'api_error': 'API错误',
'wechat_offline': '微信离线',
'wechat_online': '微信上线',
'node_offline': '节点离线',
'node_online': '节点上线'
};
const events = Array.isArray(w.event_types) ? w.event_types.map(e => eventLabels[e] || e).join(', ') : '-';
return `<tr>
<td>${w.name || '-'}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${w.url || '-'}</td>
<td>${w.format === 'bark' ? 'Bark' : '企业微信'}</td>
<td style="font-size: 12px;">${events}</td>
<td><span class="status-${w.enabled ? 'online' : 'offline'}">${w.enabled ? '启用' : '禁用'}</span></td>
<td>
<button class="btn btn-xs" onclick="editWebhook(${w.id})">编辑</button>
<button class="btn btn-xs btn-danger" onclick="deleteWebhook(${w.id})">删除</button>
</td>
</tr>`;
}).join('')}
</tbody>
</table>
`;
}
function showAddWebhookModal(webhook = null) {
const modal = document.getElementById('modal');
const title = document.getElementById('modalTitle');
const form = document.getElementById('nodeForm');
title.textContent = webhook ? '编辑 Webhook' : '添加 Webhook';
form.innerHTML = `
<input type="hidden" id="webhookId" value="${webhook ? webhook.id : ''}">
<div class="form-group">
<label>名称</label>
<input type="text" id="webhookName" class="input" value="${webhook ? webhook.name : ''}" placeholder="如:Bark告警">
</div>
<div class="form-group">
<label>URL</label>
<input type="text" id="webhookUrl" class="input" value="${webhook ? webhook.url : ''}" placeholder="https://api.day.app/xxx">
</div>
<div class="form-group">
<label>格式</label>
<select id="webhookFormat" class="select">
<option value="bark" ${webhook && webhook.format === 'bark' ? 'selected' : ''}>Bark</option>
<option value="wechat" ${webhook && webhook.format === 'wechat' ? 'selected' : ''}>企业微信</option>
</select>
</div>
<div class="form-group">
<label>启用</label>
<label class="checkbox-label">
<input type="checkbox" id="webhookEnabled" ${!webhook || webhook.enabled ? 'checked' : ''}>
启用此 Webhook
</label>
</div>
<div class="form-group">
<label>接收事件</label>
<div class="event-types">
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="api_error" ${webhook && webhook.event_types && webhook.event_types.includes('api_error') ? 'checked' : ''}> API错误</label>
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="wechat_offline" ${webhook && webhook.event_types && webhook.event_types.includes('wechat_offline') ? 'checked' : ''}> 微信离线</label>
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="wechat_online" ${webhook && webhook.event_types && webhook.event_types.includes('wechat_online') ? 'checked' : ''}> 微信上线</label>
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="node_offline" ${webhook && webhook.event_types && webhook.event_types.includes('node_offline') ? 'checked' : ''}> 节点离线</label>
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="node_online" ${webhook && webhook.event_types && webhook.event_types.includes('node_online') ? 'checked' : ''}> 节点上线</label>
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="plugin_notification" ${webhook && webhook.event_types && webhook.event_types.includes('plugin_notification') ? 'checked' : ''}> 插件通知</label>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn" onclick="saveWebhook()">保存</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
</div>
`;
modal.style.display = 'flex';
}
async function editWebhook(id) {
const result = await api(`/webhook/${id}`);
if (result.success && result.data) {
showAddWebhookModal(result.data);
}
}
async function deleteWebhook(id) {
if (!confirm('确定删除此 Webhook')) return;
const result = await api(`/webhook/${id}`, { method: 'DELETE' });
if (result.success) {
showToast('Webhook 已删除');
loadWebhookTable();
} else {
showToast('删除失败', 'error');
}
}
async function saveWebhook() {
const id = document.getElementById('webhookId').value;
const eventTypes = Array.from(document.querySelectorAll('.webhook-event:checked')).map(cb => cb.value);
const data = {
name: document.getElementById('webhookName').value,
url: document.getElementById('webhookUrl').value,
format: document.getElementById('webhookFormat').value,
enabled: document.getElementById('webhookEnabled').checked,
event_types: eventTypes
};
if (!data.url) {
showToast('请输入 URL', 'error');
return;
}
let result;
if (id) {
result = await api(`/webhook/${id}`, { method: 'PUT', body: JSON.stringify(data) });
} else {
result = await api('/webhook/', { method: 'POST', body: JSON.stringify(data) });
}
if (result.success) {
showToast(id ? 'Webhook 已更新' : 'Webhook 已添加');
closeModal();
loadWebhookTable();
} else {
showToast('保存失败: ' + (result.error || ''), 'error');
}
}
async function startLogStream() {
const result = await api('/logs/recent?count=200');
if (result.success && result.data) {
const container = document.getElementById('logContainer');
if (container) {
container.innerHTML = '';
if (result.data.length > 0) {
lastLogTime = result.data[0].timestamp;
result.data.forEach(entry => addLogEntry(entry));
}
}
}
if (logPollInterval) clearInterval(logPollInterval);
logPollInterval = setInterval(async () => {
const res = await api('/logs/recent?count=20');
if (res.success && res.data && res.data.length > 0) {
const newLogs = res.data.filter(log => {
if (!lastLogTime) return true;
return new Date(log.timestamp) > new Date(lastLogTime);
});
if (newLogs.length > 0) {
lastLogTime = newLogs[0].timestamp;
const container = document.getElementById('logContainer');
if (container) newLogs.forEach(entry => addLogEntry(entry));
}
}
}, 3000);
}
function addLogEntry(entry) {
const container = document.getElementById('logContainer');
if (!container) return;
const colors = { 'INFO': '#4a9eff', 'WARNING': '#fa8c16', 'ERROR': '#f5222d', 'SUCCESS': '#52c41a' };
const color = colors[entry.level] || '#666';
const html = `<div class="log-entry">
<span class="log-time">${entry.timestamp}</span>
<span class="log-level" style="color: ${color}">${entry.level}</span>
<span class="log-source">[${entry.source}]</span>
<span class="log-message">${entry.message}</span>
</div>`;
container.insertAdjacentHTML('afterbegin', html);
if (container.children.length > 500) container.lastChild.remove();
}
function clearLogs() {
const container = document.getElementById('logContainer');
if (container) container.innerHTML = '';
}
function initExternalPage() {
document.getElementById('externalApiUrl').textContent = `${window.location.origin}/api/v1/external/send`;
document.getElementById('webhookReceiveUrl').textContent = `${window.location.origin}/api/v1/external/webhook/{event}`;
}
async function loadPluginTable() {
const result = await api('/plugins/');
if (result.success) renderPluginTable(result.plugins || []);
}
function renderPluginTable(plugins) {
const container = document.getElementById('pluginTable');
if (!container) return;
if (!plugins.length) {
container.innerHTML = '<p class="empty">暂无插件</p>';
return;
}
const typeLabels = {
'message_handler': '消息处理',
'data_source': '数据源',
'action_trigger': '动作触发',
'ai_agent': 'AI智能体',
'custom': '自定义'
};
container.innerHTML = `
<table class="table">
<thead>
<tr><th>名称</th><th>版本</th><th>类型</th><th>描述</th><th>状态</th><th>操作</th></tr>
</thead>
<tbody>
${plugins.map(p => `
<tr>
<td>${p.name || '-'}</td>
<td>${p.version || '-'}</td>
<td>${typeLabels[p.type] || p.type || '-'}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${p.description || '-'}</td>
<td><span class="status-${p.enabled ? 'online' : 'offline'}">${p.enabled ? '启用' : '禁用'}</span></td>
<td>
<button class="btn btn-xs" onclick="togglePlugin('${p.name}')">${p.enabled ? '禁用' : '启用'}</button>
<button class="btn btn-xs" onclick="configPlugin('${p.name}')">配置</button>
<button class="btn btn-xs btn-secondary" onclick="showPluginLogs('${p.name}')">日志</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
async function togglePlugin(name) {
const result = await api(`/plugins/${name}`);
if (result.success && result.plugin) {
const action = result.plugin.enabled ? 'disable' : 'enable';
const res = await api(`/plugins/${name}/${action}`, { method: 'POST' });
if (res.success) {
showToast(res.message);
loadPluginTable();
}
}
}
function configPlugin(name) {
Promise.all([
api(`/plugins/${name}`),
api(`/plugins/${name}/schema`)
]).then(([pluginResult, schemaResult]) => {
if (pluginResult.success) {
if (schemaResult.success && schemaResult.has_form && schemaResult.schema) {
showPluginConfigForm(pluginResult.plugin, schemaResult.schema);
} else {
showPluginConfigModal(pluginResult.plugin);
}
}
});
}
function showPluginConfigForm(plugin, schema) {
const modal = document.getElementById('modal');
const title = document.getElementById('modalTitle');
const form = document.getElementById('nodeForm');
title.textContent = `配置插件: ${plugin.name}`;
let formHtml = '';
const currentConfig = plugin.config || {};
if (schema.sections) {
schema.sections.forEach((section, sIdx) => {
const sectionId = `plugin-section-${sIdx}`;
formHtml += `<div class="form-section">
<h4 class="section-header" onclick="togglePluginSection('${sectionId}')">
<span>${section.label}</span>
<span class="section-arrow" id="${sectionId}-arrow">▼</span>
</h4>
<div class="section-content" id="${sectionId}" style="display: block;">`;
section.fields.forEach(field => {
const value = currentConfig[field.name] !== undefined ? currentConfig[field.name] : field.default;
if (field.type === 'string' || field.type === 'password') {
formHtml += `<div class="form-group">
<label>${field.label}</label>
<input type="${field.type === 'password' ? 'password' : 'text'}"
id="plugin_field_${field.name}"
value="${escapeHtml(value || '')}"
placeholder="${field.placeholder || ''}"
${field.required ? 'required' : ''}>
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
</div>`;
} else if (field.type === 'number') {
formHtml += `<div class="form-group">
<label>${field.label}</label>
<input type="number"
id="plugin_field_${field.name}"
value="${value}"
min="${field.min_value || ''}"
max="${field.max_value || ''}"
${field.required ? 'required' : ''}>
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
</div>`;
} else if (field.type === 'boolean') {
formHtml += `<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="plugin_field_${field.name}" ${value ? 'checked' : ''}>
${field.label}
</label>
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
</div>`;
} else if (field.type === 'textarea') {
formHtml += `<div class="form-group">
<label>${field.label}</label>
<textarea id="plugin_field_${field.name}"
rows="3"
placeholder="${field.placeholder || ''}">${escapeHtml(value || '')}</textarea>
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
</div>`;
} else if (field.type === 'select' && field.options) {
let optionsHtml = field.options.map(opt =>
`<option value="${opt.value}" ${value === opt.value ? 'selected' : ''}>${opt.label}</option>`
).join('');
formHtml += `<div class="form-group">
<label>${field.label}</label>
<select id="plugin_field_${field.name}">${optionsHtml}</select>
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
</div>`;
} else if (field.type === 'cron') {
formHtml += `<div class="form-group">
<label>${field.label}</label>
<input type="text"
id="plugin_field_${field.name}"
value="${escapeHtml(value || '')}"
placeholder="*/5 * * * *">
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
</div>`;
} else {
formHtml += `<div class="form-group">
<label>${field.label}</label>
<input type="text"
id="plugin_field_${field.name}"
value="${escapeHtml(value || '')}"
placeholder="${field.placeholder || ''}">
</div>`;
}
});
formHtml += `</div></div>`;
});
}
formHtml += `<input type="hidden" id="pluginName" value="${plugin.name}">`;
formHtml += `<input type="hidden" id="pluginSchema" value='${JSON.stringify(schema).replace(/'/g, "&#39;")}'>`;
form.innerHTML = formHtml + `
<div class="form-actions">
<button type="button" class="btn" onclick="savePluginConfigFromForm()">保存</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
</div>
`;
modal.style.display = 'flex';
}
function togglePluginSection(sectionId) {
const content = document.getElementById(sectionId);
const arrow = document.getElementById(sectionId + '-arrow');
if (content.style.display === 'none') {
content.style.display = 'block';
arrow.textContent = '▼';
} else {
content.style.display = 'none';
arrow.textContent = '▶';
}
}
function escapeHtml(text) {
if (typeof text !== 'string') text = String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function savePluginConfigFromForm() {
const name = document.getElementById('pluginName').value;
const schema = JSON.parse(document.getElementById('pluginSchema').value);
const config = {};
if (schema.sections) {
schema.sections.forEach(section => {
section.fields.forEach(field => {
const input = document.getElementById(`plugin_field_${field.name}`);
if (!input) return;
if (field.type === 'boolean') {
config[field.name] = input.checked;
} else if (field.type === 'number') {
config[field.name] = parseFloat(input.value) || 0;
} else {
config[field.name] = input.value;
}
});
});
}
const result = await api(`/plugins/${name}/config`, {
method: 'PUT',
body: JSON.stringify({ name, config })
});
if (result.success) {
showToast('配置已保存');
closeModal();
loadPluginTable();
} else {
showToast('保存失败: ' + (result.error || ''), 'error');
}
}
function showPluginConfigModal(plugin) {
const modal = document.getElementById('modal');
const title = document.getElementById('modalTitle');
const form = document.getElementById('nodeForm');
title.textContent = `配置插件: ${plugin.name} (JSON)`;
const configJson = JSON.stringify(plugin.config || {}, null, 2);
form.innerHTML = `
<div class="form-group">
<label>配置 (JSON)</label>
<textarea id="pluginConfigJson" class="textarea" rows="6" placeholder='{"key": "value"}'>${configJson}</textarea>
</div>
<input type="hidden" id="pluginName" value="${plugin.name}">
<div class="form-actions">
<button type="button" class="btn" onclick="savePluginConfig()">保存</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
</div>
`;
modal.style.display = 'flex';
}
async function savePluginConfig() {
const name = document.getElementById('pluginName').value;
let config;
try {
config = JSON.parse(document.getElementById('pluginConfigJson').value);
} catch (e) {
showToast('JSON格式错误', 'error');
return;
}
const result = await api(`/plugins/${name}/config`, {
method: 'PUT',
body: JSON.stringify({ name, config })
});
if (result.success) {
showToast('配置已保存');
closeModal();
loadPluginTable();
} else {
showToast('保存失败: ' + (result.error || ''), 'error');
}
}
async function showPluginLogs(pluginName) {
const modal = document.getElementById('modal');
const title = document.getElementById('modalTitle');
const form = document.getElementById('nodeForm');
title.textContent = `插件日志: ${pluginName}`;
form.innerHTML = `
<div class="form-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<label>日志内容</label>
<button type="button" class="btn btn-xs" onclick="refreshPluginLogs('${pluginName}')">刷新</button>
</div>
<div id="pluginLogsContent" style="background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 4px; max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all;">
加载中...
</div>
</div>
<input type="hidden" id="logPluginName" value="${pluginName}">
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()">关闭</button>
</div>
`;
modal.style.display = 'flex';
await refreshPluginLogs(pluginName);
}
async function refreshPluginLogs(pluginName) {
const logsContent = document.getElementById('pluginLogsContent');
if (!logsContent) return;
logsContent.textContent = '加载中...';
const result = await api(`/plugins/${pluginName}/logs?lines=100`);
if (result.success) {
if (result.logs && result.logs.length > 0) {
logsContent.textContent = result.logs.join('\n');
logsContent.scrollTop = logsContent.scrollHeight;
} else {
logsContent.textContent = '暂无日志';
}
} else {
logsContent.textContent = '加载失败: ' + (result.error || '未知错误');
}
}
function showAddNodeModal(node = null) {
document.getElementById('modalTitle').textContent = '添加节点';
document.getElementById('nodeForm').reset();
document.getElementById('nodeIdOld').value = '';
document.getElementById('nodeId').disabled = false;
document.getElementById('modal').style.display = 'flex';
}
async function editNode(nodeId) {
const result = await api(`/nodes/${nodeId}`);
if (result.success) {
const node = result.data;
document.getElementById('modalTitle').textContent = '编辑节点';
document.getElementById('nodeIdOld').value = node.node_id;
document.getElementById('nodeId').value = node.node_id;
document.getElementById('nodeId').disabled = true;
document.getElementById('nodeName').value = node.name;
document.getElementById('nodeApiUrl').value = node.api_url;
document.getElementById('nodeApiKey').value = node.api_key;
document.getElementById('nodeGroup').value = node.group;
document.getElementById('nodeDesc').value = node.description || '';
document.getElementById('modal').style.display = 'flex';
}
}
async function deleteNode(nodeId) {
if (!confirm(`确定删除节点 ${nodeId}?`)) return;
const result = await api(`/nodes/${nodeId}`, { method: 'DELETE' });
if (result.success) {
showToast('节点已删除');
await loadNodesForTable();
}
}
document.getElementById('nodeForm').onsubmit = async (e) => {
e.preventDefault();
const oldId = document.getElementById('nodeIdOld').value;
const nodeData = {
node_id: document.getElementById('nodeId').value,
name: document.getElementById('nodeName').value,
api_url: document.getElementById('nodeApiUrl').value,
api_key: document.getElementById('nodeApiKey').value,
group: document.getElementById('nodeGroup').value,
description: document.getElementById('nodeDesc').value
};
let result;
if (oldId) {
result = await api(`/nodes/${oldId}`, { method: 'PUT', body: JSON.stringify(nodeData) });
} else {
result = await api('/nodes/', { method: 'POST', body: JSON.stringify(nodeData) });
}
if (result.success) {
showToast(oldId ? '节点已更新' : '节点已添加');
closeModal();
await loadNodesForTable();
} else {
showToast('失败: ' + (result.error || result.detail), 'error');
}
};
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast show ' + type;
setTimeout(() => toast.classList.remove('show'), 3000);
}
document.addEventListener('DOMContentLoaded', renderApp);