feat: support NAS imports and proxy names
This commit is contained in:
+79
-19
@@ -710,7 +710,8 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称 (Tag)</th>
|
||||
<th>解析名 (Tag)</th>
|
||||
<th>代理名</th>
|
||||
<th>协议 (Type)</th>
|
||||
<th>服务器 (Server)</th>
|
||||
<th>端口 (Port)</th>
|
||||
@@ -719,7 +720,7 @@
|
||||
</thead>
|
||||
<tbody id="proxies-list">
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||
请先点击右上角 “一键拉取” 按钮解析节点
|
||||
</td>
|
||||
</tr>
|
||||
@@ -857,14 +858,28 @@
|
||||
let subscriptions = [];
|
||||
let proxies = []; // In-memory editable proxies
|
||||
let originalDns = { clash: '', singbox: '' };
|
||||
let appConfig = { publicBaseUrl: '' };
|
||||
|
||||
// On Load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
lucide.createIcons();
|
||||
await loadAppConfig();
|
||||
loadSubscriptions();
|
||||
loadDnsTemplates();
|
||||
});
|
||||
|
||||
async function loadAppConfig() {
|
||||
try {
|
||||
const res = await fetch('/api/app-config');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
appConfig.publicBaseUrl = (data.publicBaseUrl || '').replace(/\/+$/, '');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load app config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Logging helper
|
||||
function log(message, type = 'info') {
|
||||
const logs = document.getElementById('console-logs');
|
||||
@@ -1094,6 +1109,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, ch => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
})[ch]);
|
||||
}
|
||||
|
||||
function getParsedName(node) {
|
||||
return node.name || node.tag || '';
|
||||
}
|
||||
|
||||
function getProxyName(node) {
|
||||
return node.proxyName || node.originalName || getParsedName(node);
|
||||
}
|
||||
|
||||
function renderProxiesTable() {
|
||||
const list = document.getElementById('proxies-list');
|
||||
const counter = document.getElementById('proxies-counter');
|
||||
@@ -1104,7 +1137,7 @@
|
||||
if (proxies.length === 0) {
|
||||
list.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||
未提取到任何代理节点
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1114,11 +1147,17 @@
|
||||
|
||||
proxies.forEach((node, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
const parsedName = getParsedName(node);
|
||||
const proxyName = getProxyName(node);
|
||||
const nodeType = node.type || '';
|
||||
const server = node.server || '';
|
||||
const port = node.port || node.server_port || '';
|
||||
tr.innerHTML = `
|
||||
<td style="font-weight: 500;">${node.name || node.tag}</td>
|
||||
<td><span class="node-proto proto-${(node.type || '').toLowerCase()}">${node.type}</span></td>
|
||||
<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 style="font-weight: 500;" title="${escapeHtml(parsedName)}">${escapeHtml(parsedName)}</td>
|
||||
<td title="${escapeHtml(proxyName)}">${escapeHtml(proxyName)}</td>
|
||||
<td><span class="node-proto proto-${escapeHtml(nodeType.toLowerCase())}">${escapeHtml(nodeType)}</span></td>
|
||||
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${escapeHtml(server)}</td>
|
||||
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${escapeHtml(port)}</td>
|
||||
<td>
|
||||
<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})">
|
||||
@@ -1136,7 +1175,7 @@
|
||||
}
|
||||
|
||||
function deleteProxy(idx) {
|
||||
const nodeName = proxies[idx].name || proxies[idx].tag;
|
||||
const nodeName = getParsedName(proxies[idx]);
|
||||
if (confirm(`确定要删除代理节点 "${nodeName}" 吗?\n(注:这仅会从当前页面内存中删除。如需应用更改,请在删除后点击右上角“保存并生成配置”按钮重新生成配置文件)`)) {
|
||||
log(`在内存中删除代理节点: ${nodeName}`);
|
||||
proxies.splice(idx, 1);
|
||||
@@ -1155,17 +1194,18 @@
|
||||
const container = document.getElementById('node-edit-form');
|
||||
const overlay = document.getElementById('node-drawer-overlay');
|
||||
|
||||
title.textContent = `修改节点: ${node.name || node.tag}`;
|
||||
title.textContent = `修改节点: ${getParsedName(node)}`;
|
||||
container.innerHTML = '';
|
||||
|
||||
const commonFields = [
|
||||
{ name: 'name', label: '节点 Tag/Name', val: node.name || node.tag, type: 'text' },
|
||||
{ name: 'name', label: '解析名 (Tag)', val: getParsedName(node), type: 'text' },
|
||||
{ name: 'proxyName', label: '代理名', val: getProxyName(node), type: 'text', readonly: true },
|
||||
{ name: 'server', label: '服务器 IP/域名', val: node.server, type: 'text' },
|
||||
{ name: 'port', label: '端口', val: node.port || node.server_port, type: 'number' }
|
||||
];
|
||||
|
||||
commonFields.forEach(f => {
|
||||
container.appendChild(createFormField(f.name, f.label, f.val, f.type));
|
||||
container.appendChild(createFormField(f.name, f.label, f.val, f.type, { readonly: f.readonly }));
|
||||
});
|
||||
|
||||
const t = (node.type || '').toLowerCase();
|
||||
@@ -1206,12 +1246,13 @@
|
||||
overlay.classList.add('active');
|
||||
}
|
||||
|
||||
function createFormField(name, label, value, type) {
|
||||
function createFormField(name, label, value, type, options = {}) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'form-group';
|
||||
const readonlyAttr = options.readonly ? ' readonly aria-readonly="true"' : '';
|
||||
div.innerHTML = `
|
||||
<label for="edit-${name}">${label}</label>
|
||||
<input type="${type}" id="edit-${name}" class="form-control" value="${value !== undefined ? value : ''}">
|
||||
<label for="edit-${escapeHtml(name)}">${escapeHtml(label)}</label>
|
||||
<input type="${escapeHtml(type)}" id="edit-${escapeHtml(name)}" class="form-control" value="${escapeHtml(value !== undefined ? value : '')}"${readonlyAttr}>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
@@ -1402,6 +1443,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getClientImportOrigin() {
|
||||
if (appConfig.publicBaseUrl) {
|
||||
return appConfig.publicBaseUrl;
|
||||
}
|
||||
|
||||
const localHosts = new Set(['localhost', '127.0.0.1', '::1']);
|
||||
if (!localHosts.has(window.location.hostname)) {
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
const port = window.location.port ? `:${window.location.port}` : '';
|
||||
return `${window.location.protocol}//127.0.0.1${port}`;
|
||||
}
|
||||
|
||||
function renderHistoryList(list) {
|
||||
const tbody = document.getElementById('history-list');
|
||||
tbody.innerHTML = '';
|
||||
@@ -1418,14 +1473,19 @@
|
||||
}
|
||||
|
||||
const origin = window.location.origin;
|
||||
const clientOrigin = getClientImportOrigin();
|
||||
|
||||
list.forEach(item => {
|
||||
const yamlDownloadUrl = `${origin}/api/download?file=${item.yamlFile}`;
|
||||
const jsonDownloadUrl = `${origin}/api/download?file=${item.jsonFile}`;
|
||||
const yamlFileParam = encodeURIComponent(item.yamlFile);
|
||||
const jsonFileParam = encodeURIComponent(item.jsonFile);
|
||||
const yamlDownloadUrl = `${origin}/api/download?file=${yamlFileParam}`;
|
||||
const jsonDownloadUrl = `${origin}/api/download?file=${jsonFileParam}`;
|
||||
const yamlImportProfileUrl = `${clientOrigin}/api/profile?file=${yamlFileParam}`;
|
||||
const jsonImportProfileUrl = `${clientOrigin}/api/profile?file=${jsonFileParam}`;
|
||||
|
||||
// Construct client URI schemes
|
||||
const clashImportUrl = `clash://install-config?url=${encodeURIComponent(yamlDownloadUrl)}&name=${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
|
||||
const singboxImportUrl = `sing-box://import-remote-profile?url=${encodeURIComponent(jsonDownloadUrl)}#${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
|
||||
const clashImportUrl = `clash://install-config?url=${encodeURIComponent(yamlImportProfileUrl)}&name=${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
|
||||
const singboxImportUrl = `sing-box://import-remote-profile?url=${encodeURIComponent(jsonImportProfileUrl)}#${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
|
||||
Reference in New Issue
Block a user