From 3511833fc1700890ff0da4cf38eb31ffe2fec168 Mon Sep 17 00:00:00 2001 From: Jack <2091813161@qq.com> Date: Tue, 24 Mar 2026 10:05:00 +0800 Subject: [PATCH] Refine the Sub2API remote proxy picker in settings. Replace the native select with a custom remote proxy picker that matches the account upload UI and refresh proxy status when opening the editor or picker. --- static/css/style.css | 146 ++++++++++++++++++++++++- static/js/settings.js | 233 ++++++++++++++++++++++++++++++++++------ templates/settings.html | 15 ++- 3 files changed, 356 insertions(+), 38 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 6c2b748..76305a2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -363,10 +363,154 @@ body { gap: var(--spacing-sm); } -.sub2api-remote-proxy-select { +.sub2api-remote-proxy-picker { + position: relative; flex: 1; } +.sub2api-remote-proxy-trigger { + width: 100%; + min-height: 44px; + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-sm); + cursor: pointer; + transition: all var(--transition); + text-align: left; +} + +.sub2api-remote-proxy-trigger:hover, +.sub2api-remote-proxy-trigger[aria-expanded="true"] { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px var(--primary-light); +} + +.sub2api-remote-proxy-trigger-content { + flex: 1; + min-width: 0; +} + +.sub2api-remote-proxy-trigger-arrow { + color: var(--text-muted); + font-size: 0.875rem; + flex-shrink: 0; +} + +.sub2api-remote-proxy-placeholder { + color: var(--text-muted); + font-size: 0.875rem; +} + +.sub2api-remote-proxy-summary-title-row, +.sub2api-remote-proxy-option-title-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; +} + +.sub2api-remote-proxy-summary-name, +.sub2api-remote-proxy-option-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary); +} + +.sub2api-remote-proxy-summary-meta, +.sub2api-remote-proxy-option-meta { + margin-top: 4px; + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.4; +} + +.sub2api-remote-proxy-pill { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: var(--radius-full); + background: var(--info-light); + color: var(--info-color); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.02em; +} + +.sub2api-remote-proxy-dropdown { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 20; + display: none; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + overflow: hidden; +} + +.sub2api-remote-proxy-dropdown.active { + display: block; +} + +.sub2api-remote-proxy-options { + max-height: 320px; + overflow-y: auto; + padding: var(--spacing-sm); + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.sub2api-remote-proxy-option { + width: 100%; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: inherit; + padding: 10px 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + cursor: pointer; + transition: all var(--transition-fast); + text-align: left; +} + +.sub2api-remote-proxy-option:hover { + background: var(--surface-hover); + border-color: var(--text-muted); +} + +.sub2api-remote-proxy-option.selected { + border-color: var(--primary-color); + background: var(--primary-light); +} + +.sub2api-remote-proxy-option-main { + min-width: 0; + flex: 1; +} + +.sub2api-remote-proxy-option-check { + flex-shrink: 0; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-muted); +} + +.sub2api-remote-proxy-option.selected .sub2api-remote-proxy-option-check { + color: var(--primary-color); +} + .sub2api-remote-proxy-btn { flex-shrink: 0; } diff --git a/static/js/settings.js b/static/js/settings.js index 1a1b62b..a036b6b 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -58,6 +58,9 @@ const elements = { sub2ApiServiceModalTitle: document.getElementById('sub2api-service-modal-title'), testSub2ApiServiceBtn: document.getElementById('test-sub2api-service-btn'), loadSub2ApiRemoteProxiesBtn: document.getElementById('load-sub2api-remote-proxies-btn'), + sub2ApiRemoteProxyTrigger: document.getElementById('sub2api-remote-proxy-trigger'), + sub2ApiRemoteProxyDropdown: document.getElementById('sub2api-remote-proxy-dropdown'), + sub2ApiRemoteProxyOptions: document.getElementById('sub2api-remote-proxy-options'), // Team Manager 服务管理 addTmServiceBtn: document.getElementById('add-tm-service-btn'), tmServicesTable: document.getElementById('tm-services-table'), @@ -91,8 +94,11 @@ document.addEventListener('DOMContentLoaded', () => { initEventListeners(); }); -document.addEventListener('click', () => { +document.addEventListener('click', (e) => { document.querySelectorAll('.dropdown-menu.active').forEach(m => m.classList.remove('active')); + if (!e.target.closest('.sub2api-remote-proxy-picker')) { + closeSub2ApiRemoteProxyDropdown(); + } }); // 初始化标签页 @@ -316,6 +322,23 @@ function initEventListeners() { if (elements.loadSub2ApiRemoteProxiesBtn) { elements.loadSub2ApiRemoteProxiesBtn.addEventListener('click', () => loadSub2ApiRemoteProxiesForForm()); } + if (elements.sub2ApiRemoteProxyTrigger) { + elements.sub2ApiRemoteProxyTrigger.addEventListener('click', (e) => { + e.stopPropagation(); + const opened = toggleSub2ApiRemoteProxyDropdown(); + if (opened) { + loadSub2ApiRemoteProxiesForForm({ silent: true }); + } + }); + } + if (elements.sub2ApiRemoteProxyOptions) { + elements.sub2ApiRemoteProxyOptions.addEventListener('click', (e) => { + const optionBtn = e.target.closest('.sub2api-remote-proxy-option'); + if (!optionBtn) return; + setSub2ApiDefaultRemoteProxyValue(optionBtn.dataset.proxyValue || ''); + closeSub2ApiRemoteProxyDropdown(); + }); + } } // 加载设置 @@ -1382,51 +1405,193 @@ async function handleTestCpaService() { // ============================================================================ let _sub2apiEditingId = null; +let _sub2apiRemoteProxyOptions = []; -function getSub2ApiDefaultRemoteProxySelect() { +function normalizeSub2ApiRemoteProxyValue(value) { + return value == null || value === '' ? '' : String(value); +} + +function getSub2ApiDefaultRemoteProxyInput() { return document.getElementById('sub2api-default-remote-proxy-id'); } -function resetSub2ApiRemoteProxyOptions(selectedValue = '', firstOptionText = '不设置默认代理') { - const select = getSub2ApiDefaultRemoteProxySelect(); - if (!select) return; +function getSub2ApiRemoteProxyTriggerContent() { + return document.getElementById('sub2api-remote-proxy-trigger-content'); +} - const normalizedValue = selectedValue == null ? '' : String(selectedValue); - const options = [``]; - if (normalizedValue) { - options.push(``); +function getSub2ApiRemoteProxyOptionByValue(value) { + const normalizedValue = normalizeSub2ApiRemoteProxyValue(value); + return _sub2apiRemoteProxyOptions.find(option => option.value === normalizedValue) || null; +} + +function getSub2ApiRemoteProxyStatusClass(status) { + const normalizedStatus = String(status || '').trim().toLowerCase(); + if (normalizedStatus === 'active') return 'active'; + if ([ 'inactive', 'disabled', 'offline' ].includes(normalizedStatus)) return 'disabled'; + if ([ 'error', 'failed' ].includes(normalizedStatus)) return 'error'; + if ([ 'warning', 'missing' ].includes(normalizedStatus)) return 'warning'; + return 'pending'; +} + +function buildSub2ApiRemoteProxySummary(option) { + if (!option || option.value === '') { + return '不设置默认代理'; } - select.innerHTML = options.join(''); - select.value = normalizedValue; + const protocol = option.protocol + ? `${escapeHtml(option.protocol)}` + : ''; + const status = option.status + ? `${escapeHtml(option.status)}` + : ''; + const meta = option.meta + ? `
${escapeHtml(option.meta)}
` + : ''; + + return ` +
+ ${escapeHtml(option.title)} + ${protocol} + ${status} +
+ ${meta} + `; +} + +function buildSub2ApiRemoteProxyOptionMarkup(option, selectedValue) { + const isSelected = option.value === selectedValue; + const protocol = option.protocol + ? `${escapeHtml(option.protocol)}` + : ''; + const status = option.status + ? `${escapeHtml(option.status)}` + : ''; + const meta = option.meta + ? `
${escapeHtml(option.meta)}
` + : ''; + + return ` + + `; +} + +function renderSub2ApiRemoteProxyTriggerDisplay() { + const triggerContent = getSub2ApiRemoteProxyTriggerContent(); + const input = getSub2ApiDefaultRemoteProxyInput(); + if (!triggerContent || !input) return; + + const option = getSub2ApiRemoteProxyOptionByValue(input.value); + triggerContent.innerHTML = buildSub2ApiRemoteProxySummary(option); +} + +function renderSub2ApiRemoteProxyOptionList() { + if (!elements.sub2ApiRemoteProxyOptions) return; + const selectedValue = normalizeSub2ApiRemoteProxyValue(getSub2ApiDefaultRemoteProxyInput()?.value); + elements.sub2ApiRemoteProxyOptions.innerHTML = _sub2apiRemoteProxyOptions + .map(option => buildSub2ApiRemoteProxyOptionMarkup(option, selectedValue)) + .join(''); +} + +function setSub2ApiDefaultRemoteProxyValue(value) { + const input = getSub2ApiDefaultRemoteProxyInput(); + if (!input) return; + input.value = normalizeSub2ApiRemoteProxyValue(value); + renderSub2ApiRemoteProxyTriggerDisplay(); + renderSub2ApiRemoteProxyOptionList(); +} + +function closeSub2ApiRemoteProxyDropdown() { + if (elements.sub2ApiRemoteProxyDropdown) { + elements.sub2ApiRemoteProxyDropdown.classList.remove('active'); + } + if (elements.sub2ApiRemoteProxyTrigger) { + elements.sub2ApiRemoteProxyTrigger.setAttribute('aria-expanded', 'false'); + } +} + +function toggleSub2ApiRemoteProxyDropdown() { + if (!elements.sub2ApiRemoteProxyDropdown || !elements.sub2ApiRemoteProxyTrigger) return false; + const willOpen = !elements.sub2ApiRemoteProxyDropdown.classList.contains('active'); + elements.sub2ApiRemoteProxyDropdown.classList.toggle('active', willOpen); + elements.sub2ApiRemoteProxyTrigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); + return willOpen; +} + +function resetSub2ApiRemoteProxyOptions(selectedValue = '') { + const normalizedValue = normalizeSub2ApiRemoteProxyValue(selectedValue); + _sub2apiRemoteProxyOptions = [ + { + value: '', + title: '不设置默认代理', + meta: '上传时不默认绑定远端代理', + protocol: '', + status: '', + }, + ]; + + if (normalizedValue) { + _sub2apiRemoteProxyOptions.push({ + value: normalizedValue, + title: `当前默认代理 #${normalizedValue}`, + meta: '点击“加载代理”获取远端详情', + protocol: '', + status: 'warning', + }); + } + + setSub2ApiDefaultRemoteProxyValue(normalizedValue); + closeSub2ApiRemoteProxyDropdown(); } function renderSub2ApiRemoteProxyOptions(proxies, selectedValue = '') { - const select = getSub2ApiDefaultRemoteProxySelect(); - if (!select) return; - - const normalizedValue = selectedValue == null ? '' : String(selectedValue); - const hasSelectedProxy = normalizedValue !== '' && proxies.some(proxy => String(proxy.id) === normalizedValue); - const options = [ - `` - ]; - - if (normalizedValue && !hasSelectedProxy) { - options.push(``); - } - - options.push(...proxies.map(proxy => { + const normalizedValue = normalizeSub2ApiRemoteProxyValue(selectedValue); + const remoteProxyOptions = (proxies || []).map(proxy => { const protocol = String(proxy.protocol || '').trim().toUpperCase(); const host = String(proxy.host || '').trim(); const port = proxy.port != null ? `:${proxy.port}` : ''; - const location = host ? ` - ${host}${port}` : ''; + const meta = host ? `${host}${port}` : '未提供地址'; const status = String(proxy.status || '').trim() || 'inactive'; - const label = `#${proxy.id} ${proxy.name || `Proxy ${proxy.id}`}${protocol ? ` (${protocol})` : ''}${location} [${status}]`; - return ``; - })); + return { + value: String(proxy.id), + title: `#${proxy.id} ${proxy.name || `Proxy ${proxy.id}`}`, + meta, + protocol, + status, + }; + }); - select.innerHTML = options.join(''); - select.value = normalizedValue; + _sub2apiRemoteProxyOptions = [ + { + value: '', + title: '不设置默认代理', + meta: remoteProxyOptions.length === 0 ? '当前服务下暂无可选远端代理' : '上传时不默认绑定远端代理', + protocol: '', + status: '', + }, + ]; + + if (normalizedValue && !remoteProxyOptions.some(option => option.value === normalizedValue)) { + _sub2apiRemoteProxyOptions.push({ + value: normalizedValue, + title: `当前默认代理 #${normalizedValue}`, + meta: '远端已不存在,请重新选择或清空默认代理', + protocol: '', + status: 'warning', + }); + } + + _sub2apiRemoteProxyOptions.push(...remoteProxyOptions); + setSub2ApiDefaultRemoteProxyValue(normalizedValue); } function setSub2ApiRemoteProxyLoading(loading) { @@ -1439,8 +1604,8 @@ async function loadSub2ApiRemoteProxiesForForm({ silent = false } = {}) { const id = document.getElementById('sub2api-service-id').value.trim(); const apiUrl = document.getElementById('sub2api-service-url').value.trim(); const apiKey = document.getElementById('sub2api-service-key').value.trim(); - const select = getSub2ApiDefaultRemoteProxySelect(); - const selectedValue = select ? select.value : ''; + const input = getSub2ApiDefaultRemoteProxyInput(); + const selectedValue = input ? input.value : ''; if (!apiUrl) { if (!silent) toast.error('请先填写 API URL'); @@ -1566,7 +1731,7 @@ async function deleteSub2ApiService(id, name) { async function handleSaveSub2ApiService(e) { e.preventDefault(); const id = document.getElementById('sub2api-service-id').value; - const defaultRemoteProxyValue = getSub2ApiDefaultRemoteProxySelect()?.value || ''; + const defaultRemoteProxyValue = getSub2ApiDefaultRemoteProxyInput()?.value || ''; const parsedDefaultRemoteProxyId = defaultRemoteProxyValue === '' ? null : parseInt(defaultRemoteProxyValue, 10); const data = { name: document.getElementById('sub2api-service-name').value, diff --git a/templates/settings.html b/templates/settings.html index 2838511..4242057 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -351,10 +351,19 @@
+
- +
+ +
+
+
+

与账号页上传时选择的远端代理一致;仅在未显式选择代理时作为默认值。