소스 검색

feat(sidepanel): 添加配置菜单,支持导入和导出设置功能

QLHazyCoder 4 달 전
부모
커밋
b382cbf02f
5개의 변경된 파일503개의 추가작업 그리고 102개의 파일을 삭제
  1. 1 0
      README.md
  2. 184 47
      background.js
  3. 61 0
      sidepanel/sidepanel.css
  4. 9 0
      sidepanel/sidepanel.html
  5. 248 55
      sidepanel/sidepanel.js

+ 1 - 0
README.md

@@ -215,6 +215,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 - 手动输入:使用你自定义的密码
 - 可通过输入框右侧的眼睛图标切换显示
 - 配置会自动保存,也可以点击右侧 `保存` 按钮手动保存一次
+- 右上角 `配置` 按钮支持导出当前配置到 JSON 文件,也支持从 JSON 文件覆盖导入配置
 
 扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。
 

+ 184 - 47
background.js

@@ -70,6 +70,8 @@ const PERSISTED_SETTING_DEFAULTS = {
 };
 
 const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
+const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
+const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
 
 const DEFAULT_STATE = {
   currentStep: 0, // 当前流程执行到的步骤编号。
@@ -141,6 +143,24 @@ function normalizeEmailGenerator(value = '') {
   return String(value || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck';
 }
 
+function normalizePanelMode(value = '') {
+  return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa';
+}
+
+function normalizeMailProvider(value = '') {
+  const normalized = String(value || '').trim().toLowerCase();
+  switch (normalized) {
+    case HOTMAIL_PROVIDER:
+    case '163':
+    case '163-vip':
+    case 'qq':
+    case 'inbucket':
+      return normalized;
+    default:
+      return PERSISTED_SETTING_DEFAULTS.mailProvider;
+  }
+}
+
 function normalizeLocalCpaStep9Mode(value = '') {
   return String(value || '').trim().toLowerCase() === 'bypass'
     ? 'bypass'
@@ -157,18 +177,99 @@ function normalizeCloudflareDomain(rawValue = '') {
   return value;
 }
 
+function normalizeCloudflareDomains(values) {
+  const normalizedDomains = [];
+  const seen = new Set();
+
+  for (const value of Array.isArray(values) ? values : []) {
+    const normalized = normalizeCloudflareDomain(value);
+    if (!normalized || seen.has(normalized)) continue;
+    seen.add(normalized);
+    normalizedDomains.push(normalized);
+  }
+
+  return normalizedDomains;
+}
+
+function normalizePersistentSettingValue(key, value) {
+  switch (key) {
+    case 'panelMode':
+      return normalizePanelMode(value);
+    case 'vpsUrl':
+      return String(value || '').trim();
+    case 'vpsPassword':
+      return String(value || '');
+    case 'localCpaStep9Mode':
+      return normalizeLocalCpaStep9Mode(value);
+    case 'sub2apiUrl':
+      return String(value || '').trim();
+    case 'sub2apiEmail':
+      return String(value || '').trim();
+    case 'sub2apiPassword':
+      return String(value || '');
+    case 'sub2apiGroupName':
+      return String(value || '').trim();
+    case 'customPassword':
+      return String(value || '');
+    case 'autoRunSkipFailures':
+    case 'autoRunDelayEnabled':
+      return Boolean(value);
+    case 'autoRunDelayMinutes':
+      return normalizeAutoRunDelayMinutes(value);
+    case 'mailProvider':
+      return normalizeMailProvider(value);
+    case 'emailGenerator':
+      return normalizeEmailGenerator(value);
+    case 'inbucketHost':
+      return String(value || '').trim();
+    case 'inbucketMailbox':
+      return String(value || '').trim();
+    case 'cloudflareDomain':
+      return normalizeCloudflareDomain(value);
+    case 'cloudflareDomains':
+      return normalizeCloudflareDomains(value);
+    case 'hotmailAccounts':
+      return normalizeHotmailAccounts(value);
+    default:
+      return value;
+  }
+}
+
+function buildPersistentSettingsPayload(input = {}, options = {}) {
+  const { fillDefaults = false, requireKnownKeys = false } = options;
+  if (!input || typeof input !== 'object' || Array.isArray(input)) {
+    throw new Error('\u914d\u7f6e\u5185\u5bb9\u683c\u5f0f\u65e0\u6548\u3002');
+  }
+
+  const payload = {};
+  let matchedKeyCount = 0;
+  for (const key of PERSISTED_SETTING_KEYS) {
+    if (input[key] !== undefined) {
+      payload[key] = normalizePersistentSettingValue(key, input[key]);
+      matchedKeyCount += 1;
+    } else if (fillDefaults) {
+      payload[key] = normalizePersistentSettingValue(key, PERSISTED_SETTING_DEFAULTS[key]);
+    }
+  }
+
+  if (requireKnownKeys && matchedKeyCount === 0) {
+    throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002');
+  }
+
+  if (payload.cloudflareDomains) {
+    const domains = normalizeCloudflareDomains(payload.cloudflareDomains);
+    if (payload.cloudflareDomain && !domains.includes(payload.cloudflareDomain)) {
+      domains.unshift(payload.cloudflareDomain);
+    }
+    payload.cloudflareDomains = domains;
+  }
+
+  return payload;
+}
+
 async function getPersistedSettings() {
   const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
-  return {
-    ...PERSISTED_SETTING_DEFAULTS,
-    ...stored,
-    autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
-    autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled),
-    autoRunDelayMinutes: normalizeAutoRunDelayMinutes(stored.autoRunDelayMinutes ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes),
-    emailGenerator: normalizeEmailGenerator(stored.emailGenerator ?? PERSISTED_SETTING_DEFAULTS.emailGenerator),
-    localCpaStep9Mode: normalizeLocalCpaStep9Mode(stored.localCpaStep9Mode ?? PERSISTED_SETTING_DEFAULTS.localCpaStep9Mode),
-    hotmailAccounts: normalizeHotmailAccounts(stored.hotmailAccounts),
-  };
+  return buildPersistentSettingsPayload(stored, { fillDefaults: true });
 }
 
 async function getState() {
@@ -200,28 +301,75 @@ async function setState(updates) {
 }
 
 async function setPersistentSettings(updates) {
-  const persistedUpdates = {};
-  for (const key of PERSISTED_SETTING_KEYS) {
-    if (updates[key] !== undefined) {
-      if (key === 'autoRunSkipFailures' || key === 'autoRunDelayEnabled') {
-        persistedUpdates[key] = Boolean(updates[key]);
-      } else if (key === 'autoRunDelayMinutes') {
-        persistedUpdates[key] = normalizeAutoRunDelayMinutes(updates[key]);
-      } else if (key === 'localCpaStep9Mode') {
-        persistedUpdates[key] = normalizeLocalCpaStep9Mode(updates[key]);
-      } else if (key === 'hotmailAccounts') {
-        persistedUpdates[key] = normalizeHotmailAccounts(updates[key]);
-      } else {
-        persistedUpdates[key] = updates[key];
-      }
-    }
-  }
+  const persistedUpdates = buildPersistentSettingsPayload(updates);
 
   if (Object.keys(persistedUpdates).length > 0) {
     await chrome.storage.local.set(persistedUpdates);
   }
 }
 
+function buildSettingsExportFilename(date = new Date()) {
+  const pad = (value) => String(value).padStart(2, '0');
+  return `${SETTINGS_EXPORT_FILENAME_PREFIX}-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.json`;
+}
+
+async function exportSettingsBundle() {
+  const settings = await getPersistedSettings();
+  const bundle = {
+    schemaVersion: SETTINGS_EXPORT_SCHEMA_VERSION,
+    exportedAt: new Date().toISOString(),
+    extensionVersion: chrome.runtime.getManifest().version,
+    settings,
+  };
+
+  return {
+    fileName: buildSettingsExportFilename(),
+    fileContent: JSON.stringify(bundle, null, 2),
+  };
+}
+
+async function importSettingsBundle(configBundle) {
+  const state = await ensureManualInteractionAllowed('\u5bfc\u5165\u914d\u7f6e');
+  if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
+    throw new Error('\u5f53\u524d\u6709\u6b65\u9aa4\u6b63\u5728\u6267\u884c\uff0c\u65e0\u6cd5\u5bfc\u5165\u914d\u7f6e\u3002');
+  }
+  if (!configBundle || typeof configBundle !== 'object' || Array.isArray(configBundle)) {
+    throw new Error('\u914d\u7f6e\u6587\u4ef6\u5185\u5bb9\u65e0\u6548\u3002');
+  }
+
+  const schemaVersion = Number(configBundle.schemaVersion);
+  if (schemaVersion !== SETTINGS_EXPORT_SCHEMA_VERSION) {
+    throw new Error(`\u4ec5\u652f\u6301\u5bfc\u5165 schemaVersion=${SETTINGS_EXPORT_SCHEMA_VERSION} \u7684\u914d\u7f6e\u6587\u4ef6\u3002`);
+  }
+  if (!configBundle.settings || typeof configBundle.settings !== 'object' || Array.isArray(configBundle.settings)) {
+    throw new Error('\u914d\u7f6e\u6587\u4ef6\u7f3a\u5c11 settings \u914d\u7f6e\u6bb5\u3002');
+  }
+
+  const importedSettings = buildPersistentSettingsPayload(configBundle.settings, {
+    fillDefaults: true,
+    requireKnownKeys: true,
+  });
+
+  await setPersistentSettings(importedSettings);
+
+  const sessionUpdates = {
+    ...importedSettings,
+    currentHotmailAccountId: null,
+  };
+  if (importedSettings.mailProvider === HOTMAIL_PROVIDER) {
+    sessionUpdates.email = null;
+  }
+
+  await setState(sessionUpdates);
+  broadcastDataUpdate({
+    ...importedSettings,
+    currentHotmailAccountId: null,
+    ...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}),
+  });
+
+  return getState();
+}
+
 function broadcastDataUpdate(payload) {
   chrome.runtime.sendMessage({
     type: 'DATA_UPDATED',
@@ -2459,32 +2607,21 @@ async function handleMessage(message, sender) {
     }
 
     case 'SAVE_SETTING': {
-      const updates = {};
-      if (message.payload.panelMode !== undefined) updates.panelMode = message.payload.panelMode;
-      if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
-      if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
-      if (message.payload.localCpaStep9Mode !== undefined) updates.localCpaStep9Mode = normalizeLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
-      if (message.payload.sub2apiUrl !== undefined) updates.sub2apiUrl = message.payload.sub2apiUrl;
-      if (message.payload.sub2apiEmail !== undefined) updates.sub2apiEmail = message.payload.sub2apiEmail;
-      if (message.payload.sub2apiPassword !== undefined) updates.sub2apiPassword = message.payload.sub2apiPassword;
-      if (message.payload.sub2apiGroupName !== undefined) updates.sub2apiGroupName = message.payload.sub2apiGroupName;
-      if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
-      if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
-      if (message.payload.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled);
-      if (message.payload.autoRunDelayMinutes !== undefined) updates.autoRunDelayMinutes = normalizeAutoRunDelayMinutes(message.payload.autoRunDelayMinutes);
-      if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
-      if (message.payload.emailGenerator !== undefined) updates.emailGenerator = normalizeEmailGenerator(message.payload.emailGenerator);
-      if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
-      if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
-      if (message.payload.cloudflareDomain !== undefined) updates.cloudflareDomain = normalizeCloudflareDomain(message.payload.cloudflareDomain);
-      if (message.payload.cloudflareDomains !== undefined) updates.cloudflareDomains = Array.isArray(message.payload.cloudflareDomains)
-        ? message.payload.cloudflareDomains.map(domain => normalizeCloudflareDomain(domain)).filter(Boolean)
-        : [];
+      const updates = buildPersistentSettingsPayload(message.payload || {});
       await setPersistentSettings(updates);
       await setState(updates);
       return { ok: true };
     }
 
+    case 'EXPORT_SETTINGS': {
+      return { ok: true, ...(await exportSettingsBundle()) };
+    }
+
+    case 'IMPORT_SETTINGS': {
+      const state = await importSettingsBundle(message.payload?.config || null);
+      return { ok: true, state };
+    }
+
     case 'UPSERT_HOTMAIL_ACCOUNT': {
       const account = await upsertHotmailAccount(message.payload || {});
       return { ok: true, account };

+ 61 - 0
sidepanel/sidepanel.css

@@ -142,6 +142,67 @@ header {
 [data-theme="dark"] .theme-toggle .icon-moon { display: none; }
 [data-theme="dark"] .theme-toggle .icon-sun { display: block; }
 
+.header-menu {
+  position: relative;
+  display: flex;
+  align-items: center;
+}
+
+.header-config-btn {
+  min-width: 0;
+  padding-inline: 10px;
+}
+
+.header-config-btn[aria-expanded="true"] {
+  background: var(--bg-hover);
+  color: var(--text-primary);
+}
+
+.header-dropdown {
+  position: absolute;
+  top: calc(100% + 6px);
+  right: 0;
+  min-width: 120px;
+  padding: 6px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  background: var(--bg-base);
+  border: 1px solid var(--border);
+  border-radius: var(--radius-sm);
+  box-shadow: var(--shadow-md);
+  z-index: 1300;
+}
+
+.header-dropdown[hidden] {
+  display: none !important;
+}
+
+.header-dropdown-item {
+  width: 100%;
+  padding: 8px 10px;
+  border: none;
+  border-radius: var(--radius-sm);
+  background: transparent;
+  color: var(--text-secondary);
+  font: inherit;
+  font-size: 12px;
+  font-weight: 600;
+  text-align: left;
+  cursor: pointer;
+  transition: background var(--transition), color var(--transition);
+}
+
+.header-dropdown-item:hover:not(:disabled) {
+  background: var(--bg-hover);
+  color: var(--text-primary);
+}
+
+.header-dropdown-item:disabled {
+  color: var(--text-muted);
+  cursor: not-allowed;
+}
+
 /* ============================================================
    Buttons
    ============================================================ */

+ 9 - 0
sidepanel/sidepanel.html

@@ -58,6 +58,14 @@
           <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
         </svg>
       </button>
+      <div id="config-menu-shell" class="header-menu">
+        <button id="btn-config-menu" class="btn btn-ghost btn-sm header-config-btn" type="button" aria-haspopup="menu"
+          aria-expanded="false">配置</button>
+        <div id="config-menu" class="header-dropdown" role="menu" aria-labelledby="btn-config-menu" hidden>
+          <button id="btn-export-settings" class="header-dropdown-item" type="button" role="menuitem">导出配置</button>
+          <button id="btn-import-settings" class="header-dropdown-item" type="button" role="menuitem">导入配置</button>
+        </div>
+      </div>
     </div>
   </header>
 
@@ -343,6 +351,7 @@
   </div>
 
   <div id="toast-container"></div>
+  <input id="input-import-settings-file" type="file" accept=".json,application/json" hidden />
   <script src="../hotmail-utils.js"></script>
   <script src="sidepanel.js"></script>
 </body>

+ 248 - 55
sidepanel/sidepanel.js

@@ -35,6 +35,12 @@ const autoScheduleMeta = document.getElementById('auto-schedule-meta');
 const btnAutoRunNow = document.getElementById('btn-auto-run-now');
 const btnAutoCancelSchedule = document.getElementById('btn-auto-cancel-schedule');
 const btnClearLog = document.getElementById('btn-clear-log');
+const configMenuShell = document.getElementById('config-menu-shell');
+const btnConfigMenu = document.getElementById('btn-config-menu');
+const configMenu = document.getElementById('config-menu');
+const btnExportSettings = document.getElementById('btn-export-settings');
+const btnImportSettings = document.getElementById('btn-import-settings');
+const inputImportSettingsFile = document.getElementById('input-import-settings-file');
 const selectPanelMode = document.getElementById('select-panel-mode');
 const rowVpsUrl = document.getElementById('row-vps-url');
 const inputVpsUrl = document.getElementById('input-vps-url');
@@ -120,6 +126,8 @@ let currentModalActions = [];
 let scheduledCountdownTimer = null;
 let hotmailActionInFlight = false;
 let hotmailListExpanded = false;
+let configMenuOpen = false;
+let configActionInFlight = false;
 
 const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
 const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
@@ -258,6 +266,71 @@ async function openConfirmModal({ title, message, confirmLabel = '确认', confi
   return choice === 'confirm';
 }
 
+function updateConfigMenuControls() {
+  const disabled = configActionInFlight || settingsSaveInFlight;
+  const importLocked = disabled
+    || currentAutoRun.autoRunning
+    || Object.values(getStepStatuses()).some((status) => status === 'running');
+  if (btnConfigMenu) {
+    btnConfigMenu.disabled = disabled;
+    btnConfigMenu.setAttribute('aria-expanded', String(configMenuOpen));
+  }
+  if (configMenu) {
+    configMenu.hidden = !configMenuOpen;
+  }
+  if (btnExportSettings) {
+    btnExportSettings.disabled = disabled;
+  }
+  if (btnImportSettings) {
+    btnImportSettings.disabled = importLocked;
+  }
+}
+
+function closeConfigMenu() {
+  configMenuOpen = false;
+  updateConfigMenuControls();
+}
+
+function openConfigMenu() {
+  configMenuOpen = true;
+  updateConfigMenuControls();
+}
+
+function toggleConfigMenu() {
+  configMenuOpen ? closeConfigMenu() : openConfigMenu();
+}
+
+async function waitForSettingsSaveIdle() {
+  while (settingsSaveInFlight) {
+    await new Promise((resolve) => setTimeout(resolve, 50));
+  }
+}
+
+async function flushPendingSettingsBeforeExport() {
+  clearTimeout(settingsAutoSaveTimer);
+  await waitForSettingsSaveIdle();
+  if (settingsDirty) {
+    await saveSettings({ silent: true });
+  }
+}
+
+async function settlePendingSettingsBeforeImport() {
+  clearTimeout(settingsAutoSaveTimer);
+  await waitForSettingsSaveIdle();
+}
+
+function downloadTextFile(content, fileName, mimeType = 'application/json;charset=utf-8') {
+  const blob = new Blob([content], { type: mimeType });
+  const objectUrl = URL.createObjectURL(blob);
+  const anchor = document.createElement('a');
+  anchor.href = objectUrl;
+  anchor.download = fileName;
+  document.body.appendChild(anchor);
+  anchor.click();
+  anchor.remove();
+  setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
+}
+
 function isDoneStatus(status) {
   return status === 'completed' || status === 'manual_completed' || status === 'skipped';
 }
@@ -600,6 +673,7 @@ function markSettingsDirty(isDirty = true) {
 
 function updateSaveButtonState() {
   btnSaveSettings.disabled = settingsSaveInFlight || !settingsDirty;
+  updateConfigMenuControls();
   btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存';
 }
 
@@ -707,6 +781,7 @@ function applyAutoRunStatus(payload = currentAutoRun) {
   updateAutoDelayInputState();
   syncScheduledCountdownTicker();
   updateStopButtonState(scheduled || paused || locked || Object.values(getStepStatuses()).some(status => status === 'running'));
+  updateConfigMenuControls();
 }
 
 function initializeManualStepActions() {
@@ -744,11 +819,45 @@ function initializeManualStepActions() {
 // State Restore on load
 // ============================================================
 
+function applySettingsState(state) {
+  syncLatestState(state);
+  syncAutoRunState(state);
+
+  inputEmail.value = state?.email || '';
+  syncPasswordField(state || {});
+  inputVpsUrl.value = state?.vpsUrl || '';
+  inputVpsPassword.value = state?.vpsPassword || '';
+  setLocalCpaStep9Mode(state?.localCpaStep9Mode);
+  selectPanelMode.value = state?.panelMode || 'cpa';
+  inputSub2ApiUrl.value = state?.sub2apiUrl || '';
+  inputSub2ApiEmail.value = state?.sub2apiEmail || '';
+  inputSub2ApiPassword.value = state?.sub2apiPassword || '';
+  inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
+  selectMailProvider.value = state?.mailProvider || '163';
+  selectEmailGenerator.value = state?.emailGenerator || 'duck';
+  inputInbucketHost.value = state?.inbucketHost || '';
+  inputInbucketMailbox.value = state?.inbucketMailbox || '';
+  renderCloudflareDomainOptions(state?.cloudflareDomain || '');
+  setCloudflareDomainEditMode(false, { clearInput: true });
+  inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
+  inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
+  inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
+  if (state?.autoRunTotalRuns) {
+    inputRunCount.value = String(state.autoRunTotalRuns);
+  }
+
+  applyAutoRunStatus(state);
+  markSettingsDirty(false);
+  updateAutoDelayInputState();
+  updatePanelModeUI();
+  updateMailProviderUI();
+  updateButtonStates();
+}
+
 async function restoreState() {
   try {
     const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
-    syncLatestState(state);
-    syncAutoRunState(state);
+    applySettingsState(state);
 
     if (state.oauthUrl) {
       displayOauthUrl.textContent = state.oauthUrl;
@@ -758,53 +867,6 @@ async function restoreState() {
       displayLocalhostUrl.textContent = state.localhostUrl;
       displayLocalhostUrl.classList.add('has-value');
     }
-    if (state.email) {
-      inputEmail.value = state.email;
-    }
-    syncPasswordField(state);
-    if (state.vpsUrl) {
-      inputVpsUrl.value = state.vpsUrl;
-    }
-    if (state.vpsPassword) {
-      inputVpsPassword.value = state.vpsPassword;
-    }
-    setLocalCpaStep9Mode(state.localCpaStep9Mode);
-    if (state.panelMode) {
-      selectPanelMode.value = state.panelMode;
-    }
-    if (state.sub2apiUrl) {
-      inputSub2ApiUrl.value = state.sub2apiUrl;
-    }
-    if (state.sub2apiEmail) {
-      inputSub2ApiEmail.value = state.sub2apiEmail;
-    }
-    if (state.sub2apiPassword) {
-      inputSub2ApiPassword.value = state.sub2apiPassword;
-    }
-    if (state.sub2apiGroupName) {
-      inputSub2ApiGroup.value = state.sub2apiGroupName;
-    }
-    if (state.mailProvider) {
-      selectMailProvider.value = state.mailProvider;
-    }
-    if (state.emailGenerator) {
-      selectEmailGenerator.value = state.emailGenerator;
-    }
-    if (state.inbucketHost) {
-      inputInbucketHost.value = state.inbucketHost;
-    }
-    if (state.inbucketMailbox) {
-      inputInbucketMailbox.value = state.inbucketMailbox;
-    }
-    renderCloudflareDomainOptions(state.cloudflareDomain || '');
-    setCloudflareDomainEditMode(false, { clearInput: true });
-    inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
-    inputAutoDelayEnabled.checked = Boolean(state.autoRunDelayEnabled);
-    inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state.autoRunDelayMinutes));
-    if (state.autoRunTotalRuns) {
-      inputRunCount.value = String(state.autoRunTotalRuns);
-    }
-
     if (state.stepStatuses) {
       for (const [step, status] of Object.entries(state.stepStatuses)) {
         updateStepUI(Number(step), status);
@@ -817,14 +879,8 @@ async function restoreState() {
       }
     }
 
-    applyAutoRunStatus(state);
-    markSettingsDirty(false);
-    updateAutoDelayInputState();
     updateStatusDisplay(latestState);
     updateProgressCounter();
-    updatePanelModeUI();
-    updateMailProviderUI();
-    updateButtonStates();
   } catch (err) {
     console.error('Failed to restore state:', err);
   }
@@ -1177,6 +1233,7 @@ function updateStepUI(step, status) {
 
   updateButtonStates();
   updateProgressCounter();
+  updateConfigMenuControls();
 }
 
 function updateProgressCounter() {
@@ -1397,6 +1454,93 @@ async function copyTextToClipboard(text) {
   await navigator.clipboard.writeText(value);
 }
 
+async function exportSettingsFile() {
+  closeConfigMenu();
+  configActionInFlight = true;
+  updateConfigMenuControls();
+
+  try {
+    await flushPendingSettingsBeforeExport();
+    const response = await chrome.runtime.sendMessage({
+      type: 'EXPORT_SETTINGS',
+      source: 'sidepanel',
+      payload: {},
+    });
+
+    if (response?.error) {
+      throw new Error(response.error);
+    }
+    if (!response?.fileContent || !response?.fileName) {
+      throw new Error('\u672a\u751f\u6210\u53ef\u4e0b\u8f7d\u7684\u914d\u7f6e\u6587\u4ef6\u3002');
+    }
+
+    downloadTextFile(response.fileContent, response.fileName);
+    showToast('\u914d\u7f6e\u5df2\u5bfc\u51fa\uff1a' + response.fileName, 'success', 2200);
+  } catch (err) {
+    showToast('\u5bfc\u51fa\u914d\u7f6e\u5931\u8d25\uff1a' + err.message, 'error');
+  } finally {
+    configActionInFlight = false;
+    updateConfigMenuControls();
+  }
+}
+
+async function importSettingsFromFile(file) {
+  if (!file) return;
+
+  configActionInFlight = true;
+  closeConfigMenu();
+  updateConfigMenuControls();
+
+  try {
+    await settlePendingSettingsBeforeImport();
+    const rawText = await file.text();
+
+    let parsedConfig = null;
+    try {
+      parsedConfig = JSON.parse(rawText);
+    } catch {
+      throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e0d\u662f\u6709\u6548\u7684 JSON\u3002');
+    }
+
+    const confirmed = await openConfirmModal({
+      title: '\u5bfc\u5165\u914d\u7f6e',
+      message: '\u786e\u8ba4\u5bfc\u5165\u914d\u7f6e\u6587\u4ef6 "' + file.name + '" \u5417\uff1f\u5bfc\u5165\u540e\u4f1a\u8986\u76d6\u5f53\u524d\u914d\u7f6e\u3002',
+      confirmLabel: '\u786e\u8ba4\u8986\u76d6\u5bfc\u5165',
+      confirmVariant: 'btn-danger',
+    });
+    if (!confirmed) {
+      return;
+    }
+
+    const response = await chrome.runtime.sendMessage({
+      type: 'IMPORT_SETTINGS',
+      source: 'sidepanel',
+      payload: {
+        config: parsedConfig,
+      },
+    });
+
+    if (response?.error) {
+      throw new Error(response.error);
+    }
+    if (!response?.state) {
+      throw new Error('\u5bfc\u5165\u540e\u672a\u8fd4\u56de\u6700\u65b0\u914d\u7f6e\u72b6\u6001\u3002');
+    }
+
+    applySettingsState(response.state);
+    updateStatusDisplay(latestState);
+    showToast('\u914d\u7f6e\u5df2\u5bfc\u5165\uff0c\u5f53\u524d\u914d\u7f6e\u5df2\u8986\u76d6\u3002', 'success', 2200);
+  } catch (err) {
+    showToast('\u5bfc\u5165\u914d\u7f6e\u5931\u8d25\uff1a' + err.message, 'error');
+  } finally {
+    configActionInFlight = false;
+    updateConfigMenuControls();
+    if (inputImportSettingsFile) {
+      inputImportSettingsFile.value = '';
+    }
+  }
+}
+
 async function deleteHotmailAccountsByMode(mode) {
   const isUsedMode = mode === 'used';
   const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all');
@@ -1838,6 +1982,38 @@ btnStop.addEventListener('click', async () => {
   showToast(isAutoRunScheduledPhase() ? '正在取消倒计时计划...' : '正在停止当前流程...', 'warn', 2000);
 });
 
+btnConfigMenu?.addEventListener('click', (event) => {
+  event.stopPropagation();
+  toggleConfigMenu();
+});
+
+configMenu?.addEventListener('click', (event) => {
+  event.stopPropagation();
+});
+
+btnExportSettings?.addEventListener('click', async () => {
+  if (configActionInFlight || settingsSaveInFlight) {
+    return;
+  }
+  await exportSettingsFile();
+});
+
+btnImportSettings?.addEventListener('click', async () => {
+  if (configActionInFlight || settingsSaveInFlight) {
+    return;
+  }
+  closeConfigMenu();
+  if (inputImportSettingsFile) {
+    inputImportSettingsFile.value = '';
+    inputImportSettingsFile.click();
+  }
+});
+
+inputImportSettingsFile?.addEventListener('change', async () => {
+  const file = inputImportSettingsFile.files?.[0] || null;
+  await importSettingsFromFile(file);
+});
+
 autoStartModal?.addEventListener('click', (event) => {
   if (event.target === autoStartModal) {
     resolveModalChoice(null);
@@ -2280,6 +2456,22 @@ btnTheme.addEventListener('click', () => {
   setTheme(current === 'dark' ? 'light' : 'dark');
 });
 
+document.addEventListener('click', (event) => {
+  if (!configMenuOpen) {
+    return;
+  }
+  if (configMenuShell?.contains(event.target)) {
+    return;
+  }
+  closeConfigMenu();
+});
+
+document.addEventListener('keydown', (event) => {
+  if (event.key === 'Escape' && configMenuOpen) {
+    closeConfigMenu();
+  }
+});
+
 // ============================================================
 // Init
 // ============================================================
@@ -2288,6 +2480,7 @@ initializeManualStepActions();
 initTheme();
 initHotmailListExpandedState();
 updateSaveButtonState();
+updateConfigMenuControls();
 setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE);
 restoreState().then(() => {
   syncPasswordToggleLabel();