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.
This commit is contained in:
Jack committed 2026-03-24 10:05:00 +08:00
1 parent 6a875bfced
commit 3511833fc1
3 files changed
+356 -38

No files matched your search

+145 -1
View File
@@ -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;
}
+199 -34
View File
@@ -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 = [`<option value="">${escapeHtml(firstOptionText)}</option>`];
if (normalizedValue) {
options.push(`<option value="${escapeHtml(normalizedValue)}">当前默认代理 #${escapeHtml(normalizedValue)}(点击加载详情)</option>`);
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 '<span class="sub2api-remote-proxy-placeholder">不设置默认代理</span>';
}
select.innerHTML = options.join('');
select.value = normalizedValue;
const protocol = option.protocol
? `<span class="sub2api-remote-proxy-pill">${escapeHtml(option.protocol)}</span>`
: '';
const status = option.status
? `<span class="status-badge ${getSub2ApiRemoteProxyStatusClass(option.status)}">${escapeHtml(option.status)}</span>`
: '';
const meta = option.meta
? `<div class="sub2api-remote-proxy-summary-meta">${escapeHtml(option.meta)}</div>`
: '';
return `
<div class="sub2api-remote-proxy-summary-title-row">
<span class="sub2api-remote-proxy-summary-name">${escapeHtml(option.title)}</span>
${protocol}
${status}
</div>
${meta}
`;
}
function buildSub2ApiRemoteProxyOptionMarkup(option, selectedValue) {
const isSelected = option.value === selectedValue;
const protocol = option.protocol
? `<span class="sub2api-remote-proxy-pill">${escapeHtml(option.protocol)}</span>`
: '';
const status = option.status
? `<span class="status-badge ${getSub2ApiRemoteProxyStatusClass(option.status)}">${escapeHtml(option.status)}</span>`
: '';
const meta = option.meta
? `<div class="sub2api-remote-proxy-option-meta">${escapeHtml(option.meta)}</div>`
: '';
return `
<button type="button" class="sub2api-remote-proxy-option${isSelected ? ' selected' : ''}" data-proxy-value="${escapeHtml(option.value)}">
<div class="sub2api-remote-proxy-option-main">
<div class="sub2api-remote-proxy-option-title-row">
<span class="sub2api-remote-proxy-option-name">${escapeHtml(option.title)}</span>
${protocol}
${status}
</div>
${meta}
</div>
<span class="sub2api-remote-proxy-option-check">${isSelected ? '已选' : '选择'}</span>
</button>
`;
}
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 = [
`<option value="">${escapeHtml(proxies.length === 0 ? '不设置默认代理(当前暂无远端代理)' : '不设置默认代理')}</option>`
];
if (normalizedValue && !hasSelectedProxy) {
options.push(`<option value="${escapeHtml(normalizedValue)}">当前默认代理 #${escapeHtml(normalizedValue)}(远端已不存在)</option>`);
}
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 `<option value="${proxy.id}">${escapeHtml(label)}</option>`;
}));
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,
+12 -3
View File
@@ -351,10 +351,19 @@
</div>
<div class="form-group">
<label for="sub2api-default-remote-proxy-id">默认远端代理</label>
<input type="hidden" id="sub2api-default-remote-proxy-id" value="">
<div class="sub2api-remote-proxy-row">
<select id="sub2api-default-remote-proxy-id" class="sub2api-remote-proxy-select">
<option value="">不设置默认代理</option>
</select>
<div class="sub2api-remote-proxy-picker">
<button type="button" class="sub2api-remote-proxy-trigger" id="sub2api-remote-proxy-trigger" aria-expanded="false">
<div class="sub2api-remote-proxy-trigger-content" id="sub2api-remote-proxy-trigger-content">
<span class="sub2api-remote-proxy-placeholder">不设置默认代理</span>
</div>
<span class="sub2api-remote-proxy-trigger-arrow">▾</span>
</button>
<div class="sub2api-remote-proxy-dropdown" id="sub2api-remote-proxy-dropdown">
<div class="sub2api-remote-proxy-options" id="sub2api-remote-proxy-options"></div>
</div>
</div>
<button type="button" class="btn btn-secondary sub2api-remote-proxy-btn" id="load-sub2api-remote-proxies-btn">加载代理</button>
</div>
<p class="sub2api-remote-proxy-hint">与账号页上传时选择的远端代理一致;仅在未显式选择代理时作为默认值。</p>