feat(ip-proxy): 添加 IP 代理面板的展开/收起功能及持久化状态支持

This commit is contained in:
QLHazyCoder
2026-04-28 19:48:07 +08:00
parent 1b5445c361
commit f399277047
5 changed files with 100 additions and 19 deletions
+69 -19
View File
@@ -14,6 +14,47 @@ const ipProxyActionState = {
busy: false,
action: '',
};
const IP_PROXY_SECTION_EXPANDED_STORAGE_KEY = 'multipage-ip-proxy-section-expanded';
let ipProxySectionExpanded = false;
function readIpProxySectionExpanded() {
try {
return globalThis.localStorage?.getItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY) === '1';
} catch (err) {
return false;
}
}
function persistIpProxySectionExpanded(expanded) {
try {
if (expanded) {
globalThis.localStorage?.setItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY, '1');
} else {
globalThis.localStorage?.removeItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY);
}
} catch (err) {
// Ignore storage errors; the in-memory collapsed state is still enough for this session.
}
}
function setIpProxySectionExpanded(expanded) {
ipProxySectionExpanded = Boolean(expanded);
persistIpProxySectionExpanded(ipProxySectionExpanded);
if (typeof updateIpProxyUI === 'function') {
updateIpProxyUI(latestState);
}
}
function toggleIpProxySectionExpanded() {
setIpProxySectionExpanded(!ipProxySectionExpanded);
}
function initIpProxySectionExpandedState() {
ipProxySectionExpanded = readIpProxySectionExpanded();
if (typeof updateIpProxyUI === 'function') {
updateIpProxyUI(latestState);
}
}
function normalizeIpProxyActionType(value = '') {
const normalized = String(value || '').trim().toLowerCase();
@@ -1158,6 +1199,7 @@ function setIpProxyEnabledInlineStatus(state = {}, enabled = getSelectedIpProxyE
function updateIpProxyUI(state = latestState) {
const enabled = getSelectedIpProxyEnabled();
const showSettings = enabled && ipProxySectionExpanded;
const mode = getSelectedIpProxyMode();
const service = normalizeIpProxyService(selectIpProxyService?.value || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE);
const apiModeAvailable = isIpProxyApiModeAvailable();
@@ -1175,59 +1217,67 @@ function updateIpProxyUI(state = latestState) {
if (rowIpProxyEnabled) {
rowIpProxyEnabled.style.display = '';
}
if (btnToggleIpProxySection) {
btnToggleIpProxySection.disabled = !enabled;
btnToggleIpProxySection.textContent = showSettings ? '收起设置' : '展开设置';
btnToggleIpProxySection.title = enabled
? (showSettings ? '收起 IP 代理设置' : '展开 IP 代理设置')
: '开启 IP 代理后可展开设置';
btnToggleIpProxySection.setAttribute('aria-expanded', String(showSettings));
}
if (rowIpProxyFold) {
rowIpProxyFold.style.display = enabled ? '' : 'none';
rowIpProxyFold.style.display = showSettings ? '' : 'none';
}
if (rowIpProxyService) {
rowIpProxyService.style.display = enabled ? '' : 'none';
rowIpProxyService.style.display = showSettings ? '' : 'none';
}
if (rowIpProxyMode) {
rowIpProxyMode.style.display = enabled ? '' : 'none';
rowIpProxyMode.style.display = showSettings ? '' : 'none';
}
if (rowIpProxyLayout) {
rowIpProxyLayout.style.display = enabled ? '' : 'none';
rowIpProxyLayout.style.display = showSettings ? '' : 'none';
}
if (rowIpProxyApiUrl) {
rowIpProxyApiUrl.style.display = enabled && apiModeAvailable && isApiMode ? '' : 'none';
rowIpProxyApiUrl.style.display = showSettings && apiModeAvailable && isApiMode ? '' : 'none';
}
if (rowIpProxyAccountList) {
rowIpProxyAccountList.style.display = enabled && isAccountMode && accountListAvailable ? '' : 'none';
rowIpProxyAccountList.style.display = showSettings && isAccountMode && accountListAvailable ? '' : 'none';
}
if (rowIpProxyAccountSessionPrefix) {
rowIpProxyAccountSessionPrefix.style.display = enabled && showSessionOptions ? '' : 'none';
rowIpProxyAccountSessionPrefix.style.display = showSettings && showSessionOptions ? '' : 'none';
}
if (rowIpProxyAccountLifeMinutes) {
rowIpProxyAccountLifeMinutes.style.display = enabled && showSessionOptions ? '' : 'none';
rowIpProxyAccountLifeMinutes.style.display = showSettings && showSessionOptions ? '' : 'none';
}
if (rowIpProxyPoolTargetCount) {
rowIpProxyPoolTargetCount.style.display = enabled ? '' : 'none';
rowIpProxyPoolTargetCount.style.display = showSettings ? '' : 'none';
}
if (rowIpProxyHost) {
rowIpProxyHost.style.display = enabled && isAccountMode ? '' : 'none';
rowIpProxyHost.style.display = showSettings && isAccountMode ? '' : 'none';
}
if (rowIpProxyPort) {
rowIpProxyPort.style.display = enabled && isAccountMode ? '' : 'none';
rowIpProxyPort.style.display = showSettings && isAccountMode ? '' : 'none';
}
if (rowIpProxyProtocol) {
rowIpProxyProtocol.style.display = enabled ? '' : 'none';
rowIpProxyProtocol.style.display = showSettings ? '' : 'none';
}
if (rowIpProxyUsername) {
rowIpProxyUsername.style.display = enabled && isAccountMode ? '' : 'none';
rowIpProxyUsername.style.display = showSettings && isAccountMode ? '' : 'none';
}
if (rowIpProxyPassword) {
rowIpProxyPassword.style.display = enabled && isAccountMode ? '' : 'none';
rowIpProxyPassword.style.display = showSettings && isAccountMode ? '' : 'none';
}
if (rowIpProxyRegion) {
rowIpProxyRegion.style.display = enabled && isAccountMode ? '' : 'none';
rowIpProxyRegion.style.display = showSettings && isAccountMode ? '' : 'none';
}
if (rowIpProxyActions) {
rowIpProxyActions.style.display = enabled ? '' : 'none';
rowIpProxyActions.style.display = showSettings ? '' : 'none';
}
if (ipProxyActionButtons) {
ipProxyActionButtons.style.display = enabled ? '' : 'none';
ipProxyActionButtons.style.display = showSettings ? '' : 'none';
}
if (rowIpProxyRuntimeStatus) {
rowIpProxyRuntimeStatus.style.display = enabled ? '' : 'none';
rowIpProxyRuntimeStatus.style.display = showSettings ? '' : 'none';
}
if (ipProxyLayout) {
ipProxyLayout.classList.toggle('is-account-only', !apiModeAvailable);
@@ -1254,7 +1304,7 @@ function updateIpProxyUI(state = latestState) {
ipProxyApiPanel.classList.toggle('is-disabled', !apiModeAvailable);
ipProxyApiPanel.setAttribute('aria-disabled', String(!apiModeAvailable));
ipProxyApiPanel.hidden = !apiModeAvailable;
ipProxyApiPanel.style.display = enabled && apiModeAvailable ? '' : 'none';
ipProxyApiPanel.style.display = showSettings && apiModeAvailable ? '' : 'none';
}
if (inputIpProxyApiUrl) {
inputIpProxyApiUrl.disabled = !enabled || !apiModeAvailable;
+5
View File
@@ -791,6 +791,11 @@ header {
.ip-proxy-header-actions {
flex: 0 0 auto;
align-items: center;
}
#btn-toggle-ip-proxy-section {
white-space: nowrap;
}
.ip-proxy-fold {
+2
View File
@@ -454,6 +454,8 @@
<span class="data-value">用于浏览器代理接管与出口切换</span>
</div>
<div id="row-ip-proxy-enabled" class="section-mini-actions ip-proxy-header-actions">
<button id="btn-toggle-ip-proxy-section" class="btn btn-ghost btn-xs" type="button"
aria-expanded="false" aria-controls="row-ip-proxy-fold">展开设置</button>
<label class="toggle-switch" for="input-ip-proxy-enabled" title="启用或禁用 IP 代理接管">
<input type="checkbox" id="input-ip-proxy-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
+10
View File
@@ -97,6 +97,7 @@ const rowSub2ApiDefaultProxy = document.getElementById('row-sub2api-default-prox
const inputSub2ApiDefaultProxy = document.getElementById('input-sub2api-default-proxy');
const rowIpProxyEnabled = document.getElementById('row-ip-proxy-enabled');
const inputIpProxyEnabled = document.getElementById('input-ip-proxy-enabled');
const btnToggleIpProxySection = document.getElementById('btn-toggle-ip-proxy-section');
const ipProxyEnabledStatus = document.getElementById('ip-proxy-enabled-status');
const ipProxyEnabledStatusDot = document.getElementById('ip-proxy-enabled-status-dot');
const ipProxyEnabledStatusText = document.getElementById('ip-proxy-enabled-status-text');
@@ -5210,6 +5211,12 @@ btnToggleIpProxyPassword?.addEventListener('click', () => {
syncIpProxyPasswordToggleLabel();
});
btnToggleIpProxySection?.addEventListener('click', () => {
if (typeof toggleIpProxySectionExpanded === 'function') {
toggleIpProxySectionExpanded();
}
});
btnMailLogin?.addEventListener('click', async () => {
const config = getMailProviderLoginConfig();
const loginUrl = getMailProviderLoginUrl();
@@ -7033,6 +7040,9 @@ bindPasswordVisibilityToggles();
initTheme();
initHotmailListExpandedState();
initMail2925ListExpandedState();
if (typeof initIpProxySectionExpandedState === 'function') {
initIpProxySectionExpandedState();
}
updateSaveButtonState();
updateConfigMenuControls();
setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE);
@@ -7,6 +7,7 @@ const {
} = require('../mail-provider-utils');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
const ipProxyPanelSource = fs.readFileSync('sidepanel/ip-proxy-panel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
@@ -74,6 +75,9 @@ test('sidepanel renders IP proxy as a standalone card after sms verification wit
const cloudflareSectionIndex = html.indexOf('id="cloudflare-temp-email-section"');
assert.match(html, /id="ip-proxy-section" class="data-card ip-proxy-card"/);
assert.match(html, /id="btn-toggle-ip-proxy-section"/);
assert.match(html, /aria-controls="row-ip-proxy-fold"/);
assert.match(html, />展开设置<\/button>/);
assert.ok(phoneToggleIndex >= 0);
assert.ok(ipProxySectionIndex > phoneToggleIndex);
assert.ok(ipProxyToggleIndex > phoneToggleIndex);
@@ -82,6 +86,16 @@ test('sidepanel renders IP proxy as a standalone card after sms verification wit
assert.doesNotMatch(html, /id="row-ip-proxy-runtime-status"/);
});
test('IP proxy standalone card supports persisted collapse control', () => {
assert.match(ipProxyPanelSource, /IP_PROXY_SECTION_EXPANDED_STORAGE_KEY = 'multipage-ip-proxy-section-expanded'/);
assert.match(ipProxyPanelSource, /let ipProxySectionExpanded = false/);
assert.match(ipProxyPanelSource, /const showSettings = enabled && ipProxySectionExpanded/);
assert.match(ipProxyPanelSource, /rowIpProxyFold\.style\.display = showSettings \? '' : 'none'/);
assert.match(ipProxyPanelSource, /btnToggleIpProxySection\.setAttribute\('aria-expanded', String\(showSettings\)\)/);
assert.match(sidepanelSource, /btnToggleIpProxySection\?\.addEventListener\('click', \(\) => \{\s*if \(typeof toggleIpProxySectionExpanded === 'function'\)/);
assert.match(sidepanelSource, /initIpProxySectionExpandedState\(\)/);
});
test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch', () => {
const api = new Function(`
const inputPhoneVerificationEnabled = { checked: false };