2025-12-10 20:52:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<PageContainer>
|
2026-02-15 18:46:26 +08:00
|
|
|
|
<div class="relative flex gap-6">
|
|
|
|
|
|
<!-- 主内容 -->
|
|
|
|
|
|
<div class="flex-1 min-w-0">
|
|
|
|
|
|
<PageHeader
|
|
|
|
|
|
title="系统设置"
|
|
|
|
|
|
description="管理系统级别的配置和参数"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mt-6 space-y-6">
|
|
|
|
|
|
<!-- 站点信息 -->
|
|
|
|
|
|
<SiteInfoSection
|
|
|
|
|
|
id="section-site-info"
|
|
|
|
|
|
:site-name="systemConfig.site_name"
|
|
|
|
|
|
:site-subtitle="systemConfig.site_subtitle"
|
|
|
|
|
|
:loading="siteInfoLoading"
|
|
|
|
|
|
:has-changes="hasSiteInfoChanges"
|
|
|
|
|
|
@save="saveSiteInfo"
|
|
|
|
|
|
@update:site-name="systemConfig.site_name = $event"
|
|
|
|
|
|
@update:site-subtitle="systemConfig.site_subtitle = $event"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-02-26 01:20:24 +08:00
|
|
|
|
<!-- 数据管理 -->
|
2026-05-16 02:09:01 +08:00
|
|
|
|
<DataManagementSection
|
|
|
|
|
|
id="section-data-mgmt"
|
|
|
|
|
|
:config-export-loading="exportLoading"
|
|
|
|
|
|
:config-import-loading="importLoading"
|
|
|
|
|
|
:users-export-loading="exportUsersLoading"
|
|
|
|
|
|
:users-import-loading="importUsersLoading"
|
|
|
|
|
|
:aggregate-export-loading="exportAggregateLoading"
|
|
|
|
|
|
:aggregate-import-loading="importAggregateLoading"
|
|
|
|
|
|
@export="handleDataExport"
|
|
|
|
|
|
@file-select="handleDataFileSelect"
|
|
|
|
|
|
/>
|
2026-02-26 01:20:24 +08:00
|
|
|
|
|
2026-02-15 18:46:26 +08:00
|
|
|
|
<!-- 网络代理 -->
|
|
|
|
|
|
<ProxyConfigSection
|
|
|
|
|
|
id="section-proxy"
|
|
|
|
|
|
:proxy-node-id="systemConfig.system_proxy_node_id"
|
|
|
|
|
|
:online-nodes="proxyNodesStore.onlineNodes"
|
2026-04-12 16:02:38 +08:00
|
|
|
|
:all-nodes="proxyNodesStore.nodes"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
:loading="proxyConfigLoading"
|
|
|
|
|
|
:has-changes="hasProxyConfigChanges"
|
|
|
|
|
|
@save="saveProxyConfig"
|
|
|
|
|
|
@update:proxy-node-id="systemConfig.system_proxy_node_id = $event"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 基础配置 -->
|
|
|
|
|
|
<BasicConfigSection
|
|
|
|
|
|
id="section-basic"
|
2026-03-08 00:05:48 +08:00
|
|
|
|
:default-user-initial-gift-usd="systemConfig.default_user_initial_gift_usd"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
:rate-limit-per-minute="systemConfig.rate_limit_per_minute"
|
|
|
|
|
|
:enable-registration="systemConfig.enable_registration"
|
2026-03-12 01:11:35 +08:00
|
|
|
|
:password-policy-level="systemConfig.password_policy_level"
|
2026-05-15 01:54:20 +08:00
|
|
|
|
:turnstile-enabled="systemConfig.turnstile_enabled"
|
|
|
|
|
|
:turnstile-site-key="systemConfig.turnstile_site_key"
|
|
|
|
|
|
:turnstile-secret-key="systemConfig.turnstile_secret_key"
|
|
|
|
|
|
:turnstile-secret-configured="systemConfig.turnstile_secret_key_is_set"
|
|
|
|
|
|
:turnstile-allowed-hostnames-str="turnstileAllowedHostnamesStr"
|
2026-05-16 17:37:58 +08:00
|
|
|
|
:referral-enabled="systemConfig.referral_enabled"
|
|
|
|
|
|
:referral-reward-mode="systemConfig.referral_reward_mode"
|
|
|
|
|
|
:referral-recharge-percent="systemConfig.referral_recharge_percent"
|
|
|
|
|
|
:referral-headcount-amount-usd="systemConfig.referral_headcount_amount_usd"
|
|
|
|
|
|
:referral-headcount-trigger="systemConfig.referral_headcount_trigger"
|
|
|
|
|
|
:registration-privacy-policy-enabled="systemConfig.registration_privacy_policy_enabled"
|
|
|
|
|
|
:registration-privacy-policy-format="systemConfig.registration_privacy_policy_format"
|
|
|
|
|
|
:registration-privacy-policy-content="systemConfig.registration_privacy_policy_content"
|
|
|
|
|
|
:registration-privacy-policy-version="systemConfig.registration_privacy_policy_version"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
:auto-delete-expired-keys="systemConfig.auto_delete_expired_keys"
|
|
|
|
|
|
:enable-format-conversion="systemConfig.enable_format_conversion"
|
2026-05-10 12:40:52 +08:00
|
|
|
|
:enable-openai-image-sync-heartbeat="systemConfig.enable_openai_image_sync_heartbeat"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
:loading="basicConfigLoading"
|
|
|
|
|
|
:has-changes="hasBasicConfigChanges"
|
|
|
|
|
|
@save="saveBasicConfig"
|
2026-03-08 00:05:48 +08:00
|
|
|
|
@update:default-user-initial-gift-usd="systemConfig.default_user_initial_gift_usd = $event"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
@update:rate-limit-per-minute="systemConfig.rate_limit_per_minute = $event"
|
|
|
|
|
|
@update:enable-registration="systemConfig.enable_registration = $event"
|
2026-03-12 01:11:35 +08:00
|
|
|
|
@update:password-policy-level="systemConfig.password_policy_level = $event"
|
2026-05-15 01:54:20 +08:00
|
|
|
|
@update:turnstile-enabled="systemConfig.turnstile_enabled = $event"
|
|
|
|
|
|
@update:turnstile-site-key="systemConfig.turnstile_site_key = $event"
|
|
|
|
|
|
@update:turnstile-secret-key="systemConfig.turnstile_secret_key = $event"
|
|
|
|
|
|
@update:turnstile-allowed-hostnames-str="turnstileAllowedHostnamesStr = $event"
|
|
|
|
|
|
@clear-turnstile-secret="clearTurnstileSecret"
|
2026-05-16 17:37:58 +08:00
|
|
|
|
@update:referral-enabled="systemConfig.referral_enabled = $event"
|
|
|
|
|
|
@update:referral-reward-mode="systemConfig.referral_reward_mode = $event"
|
|
|
|
|
|
@update:referral-recharge-percent="systemConfig.referral_recharge_percent = $event"
|
|
|
|
|
|
@update:referral-headcount-amount-usd="systemConfig.referral_headcount_amount_usd = $event"
|
|
|
|
|
|
@update:referral-headcount-trigger="systemConfig.referral_headcount_trigger = $event"
|
|
|
|
|
|
@update:registration-privacy-policy-enabled="systemConfig.registration_privacy_policy_enabled = $event"
|
|
|
|
|
|
@update:registration-privacy-policy-format="systemConfig.registration_privacy_policy_format = $event"
|
|
|
|
|
|
@update:registration-privacy-policy-content="systemConfig.registration_privacy_policy_content = $event"
|
|
|
|
|
|
@update:registration-privacy-policy-version="systemConfig.registration_privacy_policy_version = $event"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
@update:auto-delete-expired-keys="systemConfig.auto_delete_expired_keys = $event"
|
|
|
|
|
|
@update:enable-format-conversion="systemConfig.enable_format_conversion = $event"
|
2026-05-10 12:40:52 +08:00
|
|
|
|
@update:enable-openai-image-sync-heartbeat="systemConfig.enable_openai_image_sync_heartbeat = $event"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 请求记录配置 -->
|
|
|
|
|
|
<RequestLogSection
|
|
|
|
|
|
id="section-request-log"
|
|
|
|
|
|
:request-record-level="systemConfig.request_record_level"
|
|
|
|
|
|
:max-request-body-size-k-b="maxRequestBodySizeKB"
|
|
|
|
|
|
:max-response-body-size-k-b="maxResponseBodySizeKB"
|
|
|
|
|
|
:sensitive-headers-str="sensitiveHeadersStr"
|
|
|
|
|
|
:loading="logConfigLoading"
|
|
|
|
|
|
:has-changes="hasLogConfigChanges"
|
|
|
|
|
|
@save="saveLogConfig"
|
|
|
|
|
|
@update:request-record-level="systemConfig.request_record_level = $event"
|
|
|
|
|
|
@update:max-request-body-size-k-b="maxRequestBodySizeKB = $event"
|
|
|
|
|
|
@update:max-response-body-size-k-b="maxResponseBodySizeKB = $event"
|
|
|
|
|
|
@update:sensitive-headers-str="sensitiveHeadersStr = $event"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 请求记录清理策略 -->
|
|
|
|
|
|
<CleanupPolicySection
|
|
|
|
|
|
id="section-cleanup"
|
|
|
|
|
|
:enable-auto-cleanup="systemConfig.enable_auto_cleanup"
|
|
|
|
|
|
:detail-log-retention-days="systemConfig.detail_log_retention_days"
|
|
|
|
|
|
:compressed-log-retention-days="systemConfig.compressed_log_retention_days"
|
|
|
|
|
|
:header-retention-days="systemConfig.header_retention_days"
|
|
|
|
|
|
:log-retention-days="systemConfig.log_retention_days"
|
|
|
|
|
|
:cleanup-batch-size="systemConfig.cleanup_batch_size"
|
|
|
|
|
|
:audit-log-retention-days="systemConfig.audit_log_retention_days"
|
2026-03-14 01:31:39 +08:00
|
|
|
|
:request-candidates-retention-days="systemConfig.request_candidates_retention_days"
|
|
|
|
|
|
:request-candidates-cleanup-batch-size="systemConfig.request_candidates_cleanup_batch_size"
|
2026-05-08 22:57:00 +08:00
|
|
|
|
:proxy-node-metrics-1m-retention-days="systemConfig.proxy_node_metrics_1m_retention_days"
|
|
|
|
|
|
:proxy-node-metrics-1h-retention-days="systemConfig.proxy_node_metrics_1h_retention_days"
|
|
|
|
|
|
:proxy-node-metrics-cleanup-batch-size="systemConfig.proxy_node_metrics_cleanup_batch_size"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
:loading="cleanupConfigLoading"
|
|
|
|
|
|
:has-changes="hasCleanupConfigChanges"
|
|
|
|
|
|
@save="saveCleanupConfig"
|
|
|
|
|
|
@toggle-auto-cleanup="handleAutoCleanupToggle"
|
|
|
|
|
|
@update:detail-log-retention-days="systemConfig.detail_log_retention_days = $event"
|
|
|
|
|
|
@update:compressed-log-retention-days="systemConfig.compressed_log_retention_days = $event"
|
|
|
|
|
|
@update:header-retention-days="systemConfig.header_retention_days = $event"
|
|
|
|
|
|
@update:log-retention-days="systemConfig.log_retention_days = $event"
|
|
|
|
|
|
@update:cleanup-batch-size="systemConfig.cleanup_batch_size = $event"
|
|
|
|
|
|
@update:audit-log-retention-days="systemConfig.audit_log_retention_days = $event"
|
2026-03-14 01:31:39 +08:00
|
|
|
|
@update:request-candidates-retention-days="systemConfig.request_candidates_retention_days = $event"
|
|
|
|
|
|
@update:request-candidates-cleanup-batch-size="systemConfig.request_candidates_cleanup_batch_size = $event"
|
2026-05-08 22:57:00 +08:00
|
|
|
|
@update:proxy-node-metrics-1m-retention-days="systemConfig.proxy_node_metrics_1m_retention_days = $event"
|
|
|
|
|
|
@update:proxy-node-metrics-1h-retention-days="systemConfig.proxy_node_metrics_1h_retention_days = $event"
|
|
|
|
|
|
@update:proxy-node-metrics-cleanup-batch-size="systemConfig.proxy_node_metrics_cleanup_batch_size = $event"
|
2026-02-15 18:46:26 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 定时任务 -->
|
|
|
|
|
|
<ScheduledTasksSection
|
|
|
|
|
|
id="section-scheduled"
|
|
|
|
|
|
:scheduled-tasks="scheduledTasks"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 系统版本信息 -->
|
|
|
|
|
|
<SystemInfoSection
|
|
|
|
|
|
id="section-sysinfo"
|
|
|
|
|
|
:system-version="systemVersion"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-15 18:46:26 +08:00
|
|
|
|
<!-- 右侧悬浮目录 -->
|
|
|
|
|
|
<nav class="hidden lg:block w-44 shrink-0">
|
|
|
|
|
|
<div class="sticky top-1/2 -translate-y-1/2">
|
|
|
|
|
|
<div class="relative">
|
|
|
|
|
|
<!-- 竖线:通过绝对定位,以圆点中心为基准 -->
|
|
|
|
|
|
<div class="absolute right-[3px] top-0 bottom-0 w-px bg-border" />
|
|
|
|
|
|
<ul class="relative text-sm">
|
|
|
|
|
|
<li
|
|
|
|
|
|
v-for="item in tocItems"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="relative flex items-center justify-end w-full text-right pr-4 pl-2 py-1.5 transition-all duration-200"
|
|
|
|
|
|
:class="activeSection === item.id
|
|
|
|
|
|
? 'text-primary font-medium'
|
|
|
|
|
|
: 'text-muted-foreground hover:text-foreground'"
|
|
|
|
|
|
@click="scrollToSection(item.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.label }}
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="absolute right-0 w-[7px] h-[7px] rounded-full transition-all duration-200"
|
|
|
|
|
|
:class="activeSection === item.id ? 'bg-primary scale-125' : 'bg-border'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</nav>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</div>
|
2025-12-16 18:33:14 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 导入配置对话框 -->
|
2026-02-14 20:06:38 +08:00
|
|
|
|
<ConfigImportDialog
|
|
|
|
|
|
:import-dialog-open="importDialogOpen"
|
|
|
|
|
|
:import-result-dialog-open="importResultDialogOpen"
|
|
|
|
|
|
:import-preview="importPreview"
|
|
|
|
|
|
:import-result="importResult"
|
|
|
|
|
|
:merge-mode="mergeMode"
|
|
|
|
|
|
:merge-mode-select-open="mergeModeSelectOpen"
|
|
|
|
|
|
:import-loading="importLoading"
|
|
|
|
|
|
@confirm="confirmImport"
|
|
|
|
|
|
@update:import-dialog-open="importDialogOpen = $event"
|
|
|
|
|
|
@update:import-result-dialog-open="importResultDialogOpen = $event"
|
|
|
|
|
|
@update:merge-mode="mergeMode = $event"
|
|
|
|
|
|
@update:merge-mode-select-open="mergeModeSelectOpen = $event"
|
|
|
|
|
|
/>
|
2025-12-16 18:33:14 +08:00
|
|
|
|
|
2025-12-17 19:15:08 +08:00
|
|
|
|
<!-- 用户数据导入对话框 -->
|
2026-02-14 20:06:38 +08:00
|
|
|
|
<UsersImportDialog
|
|
|
|
|
|
:import-users-dialog-open="importUsersDialogOpen"
|
|
|
|
|
|
:import-users-result-dialog-open="importUsersResultDialogOpen"
|
|
|
|
|
|
:import-users-preview="importUsersPreview"
|
|
|
|
|
|
:import-users-result="importUsersResult"
|
|
|
|
|
|
:users-merge-mode="usersMergeMode"
|
|
|
|
|
|
:users-merge-mode-select-open="usersMergeModeSelectOpen"
|
|
|
|
|
|
:import-users-loading="importUsersLoading"
|
|
|
|
|
|
@confirm="confirmImportUsers"
|
|
|
|
|
|
@update:import-users-dialog-open="importUsersDialogOpen = $event"
|
|
|
|
|
|
@update:import-users-result-dialog-open="importUsersResultDialogOpen = $event"
|
|
|
|
|
|
@update:users-merge-mode="usersMergeMode = $event"
|
|
|
|
|
|
@update:users-merge-mode-select-open="usersMergeModeSelectOpen = $event"
|
|
|
|
|
|
/>
|
2026-05-16 02:09:01 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 聚合数据导入对话框 -->
|
|
|
|
|
|
<AggregateImportDialog
|
|
|
|
|
|
:aggregate-import-dialog-open="aggregateImportDialogOpen"
|
|
|
|
|
|
:aggregate-import-result-dialog-open="aggregateImportResultDialogOpen"
|
|
|
|
|
|
:aggregate-import-preview="aggregateImportPreview"
|
|
|
|
|
|
:aggregate-import-result="aggregateImportResult"
|
|
|
|
|
|
:aggregate-merge-mode="aggregateMergeMode"
|
|
|
|
|
|
:aggregate-merge-mode-select-open="aggregateMergeModeSelectOpen"
|
|
|
|
|
|
:import-aggregate-loading="importAggregateLoading"
|
|
|
|
|
|
@confirm="confirmImportAggregate"
|
|
|
|
|
|
@update:aggregate-import-dialog-open="aggregateImportDialogOpen = $event"
|
|
|
|
|
|
@update:aggregate-import-result-dialog-open="aggregateImportResultDialogOpen = $event"
|
|
|
|
|
|
@update:aggregate-merge-mode="aggregateMergeMode = $event"
|
|
|
|
|
|
@update:aggregate-merge-mode-select-open="aggregateMergeModeSelectOpen = $event"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</PageContainer>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-02-15 18:46:26 +08:00
|
|
|
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
2026-02-14 20:06:38 +08:00
|
|
|
|
import { PageHeader, PageContainer } from '@/components/layout'
|
2026-02-07 14:30:46 +08:00
|
|
|
|
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
// Composables
|
|
|
|
|
|
import { useSystemConfig } from './system-settings/composables/useSystemConfig'
|
|
|
|
|
|
import { useConfigExportImport } from './system-settings/composables/useConfigExportImport'
|
|
|
|
|
|
import { useScheduledTasks } from './system-settings/composables/useScheduledTasks'
|
|
|
|
|
|
|
|
|
|
|
|
// Section components
|
|
|
|
|
|
import SiteInfoSection from './system-settings/SiteInfoSection.vue'
|
2026-02-26 01:20:24 +08:00
|
|
|
|
import DataManagementSection from './system-settings/DataManagementSection.vue'
|
2026-02-14 20:06:38 +08:00
|
|
|
|
import ProxyConfigSection from './system-settings/ProxyConfigSection.vue'
|
|
|
|
|
|
import BasicConfigSection from './system-settings/BasicConfigSection.vue'
|
|
|
|
|
|
import RequestLogSection from './system-settings/RequestLogSection.vue'
|
|
|
|
|
|
import CleanupPolicySection from './system-settings/CleanupPolicySection.vue'
|
|
|
|
|
|
import ScheduledTasksSection from './system-settings/ScheduledTasksSection.vue'
|
|
|
|
|
|
import SystemInfoSection from './system-settings/SystemInfoSection.vue'
|
|
|
|
|
|
|
|
|
|
|
|
// Dialog components
|
|
|
|
|
|
import ConfigImportDialog from './system-settings/ConfigImportDialog.vue'
|
|
|
|
|
|
import UsersImportDialog from './system-settings/UsersImportDialog.vue'
|
2026-05-16 02:09:01 +08:00
|
|
|
|
import AggregateImportDialog from './system-settings/AggregateImportDialog.vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
const proxyNodesStore = useProxyNodesStore()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-15 18:46:26 +08:00
|
|
|
|
// TOC 目录导航
|
|
|
|
|
|
const tocItems = [
|
|
|
|
|
|
{ id: 'section-site-info', label: '站点信息' },
|
2026-02-26 01:20:24 +08:00
|
|
|
|
{ id: 'section-data-mgmt', label: '数据管理' },
|
2026-02-15 18:46:26 +08:00
|
|
|
|
{ id: 'section-proxy', label: '网络代理' },
|
|
|
|
|
|
{ id: 'section-basic', label: '基础配置' },
|
|
|
|
|
|
{ id: 'section-request-log', label: '请求记录' },
|
|
|
|
|
|
{ id: 'section-cleanup', label: '记录清理策略' },
|
|
|
|
|
|
{ id: 'section-scheduled', label: '定时任务' },
|
|
|
|
|
|
{ id: 'section-sysinfo', label: '系统信息' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const activeSection = ref(tocItems[0].id)
|
|
|
|
|
|
let observer: IntersectionObserver | null = null
|
|
|
|
|
|
|
|
|
|
|
|
function getScrollContainer(): HTMLElement | null {
|
|
|
|
|
|
return document.querySelector('.app-shell__content')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToSection(id: string) {
|
|
|
|
|
|
const el = document.getElementById(id)
|
|
|
|
|
|
const container = getScrollContainer()
|
|
|
|
|
|
if (el && container) {
|
|
|
|
|
|
const offset = 80
|
|
|
|
|
|
const top = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop - offset
|
|
|
|
|
|
container.scrollTo({ top, behavior: 'smooth' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setupScrollSpy() {
|
|
|
|
|
|
const sectionIds = tocItems.map(item => item.id)
|
|
|
|
|
|
const container = getScrollContainer()
|
|
|
|
|
|
if (!container) return
|
|
|
|
|
|
|
|
|
|
|
|
const visibleSections = new Set<string>()
|
|
|
|
|
|
|
|
|
|
|
|
observer = new IntersectionObserver(
|
|
|
|
|
|
(entries) => {
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
|
visibleSections.add(entry.target.id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
visibleSections.delete(entry.target.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const topId = sectionIds.find(id => visibleSections.has(id))
|
|
|
|
|
|
if (topId) {
|
|
|
|
|
|
activeSection.value = topId
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ root: container, rootMargin: '-80px 0px -60% 0px', threshold: 0 }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for (const id of sectionIds) {
|
|
|
|
|
|
const el = document.getElementById(id)
|
|
|
|
|
|
if (el) observer.observe(el)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
// System config composable
|
|
|
|
|
|
const {
|
|
|
|
|
|
systemConfig,
|
|
|
|
|
|
systemVersion,
|
|
|
|
|
|
siteInfoLoading,
|
|
|
|
|
|
proxyConfigLoading,
|
|
|
|
|
|
basicConfigLoading,
|
|
|
|
|
|
logConfigLoading,
|
|
|
|
|
|
cleanupConfigLoading,
|
|
|
|
|
|
hasSiteInfoChanges,
|
|
|
|
|
|
hasProxyConfigChanges,
|
|
|
|
|
|
hasBasicConfigChanges,
|
|
|
|
|
|
hasLogConfigChanges,
|
|
|
|
|
|
hasCleanupConfigChanges,
|
|
|
|
|
|
maxRequestBodySizeKB,
|
|
|
|
|
|
maxResponseBodySizeKB,
|
|
|
|
|
|
sensitiveHeadersStr,
|
2026-05-15 01:54:20 +08:00
|
|
|
|
turnstileAllowedHostnamesStr,
|
2026-02-14 20:06:38 +08:00
|
|
|
|
loadSystemConfig,
|
|
|
|
|
|
loadSystemVersion,
|
|
|
|
|
|
saveSiteInfo,
|
|
|
|
|
|
saveProxyConfig,
|
|
|
|
|
|
saveBasicConfig,
|
2026-05-15 01:54:20 +08:00
|
|
|
|
clearTurnstileSecret,
|
2026-02-14 20:06:38 +08:00
|
|
|
|
saveLogConfig,
|
|
|
|
|
|
saveCleanupConfig,
|
|
|
|
|
|
handleAutoCleanupToggle,
|
|
|
|
|
|
} = useSystemConfig()
|
|
|
|
|
|
|
2026-05-16 02:09:01 +08:00
|
|
|
|
// 数据导出/导入 composable
|
2026-02-14 20:06:38 +08:00
|
|
|
|
const {
|
|
|
|
|
|
exportLoading,
|
|
|
|
|
|
importLoading,
|
|
|
|
|
|
importDialogOpen,
|
|
|
|
|
|
importResultDialogOpen,
|
|
|
|
|
|
importPreview,
|
|
|
|
|
|
importResult,
|
|
|
|
|
|
mergeMode,
|
|
|
|
|
|
mergeModeSelectOpen,
|
|
|
|
|
|
handleExportConfig,
|
|
|
|
|
|
handleConfigFileSelect,
|
|
|
|
|
|
confirmImport,
|
|
|
|
|
|
exportUsersLoading,
|
|
|
|
|
|
importUsersLoading,
|
|
|
|
|
|
importUsersDialogOpen,
|
|
|
|
|
|
importUsersResultDialogOpen,
|
|
|
|
|
|
importUsersPreview,
|
|
|
|
|
|
importUsersResult,
|
|
|
|
|
|
usersMergeMode,
|
|
|
|
|
|
usersMergeModeSelectOpen,
|
|
|
|
|
|
handleExportUsers,
|
|
|
|
|
|
handleUsersFileSelect,
|
|
|
|
|
|
confirmImportUsers,
|
2026-05-16 02:09:01 +08:00
|
|
|
|
exportAggregateLoading,
|
|
|
|
|
|
importAggregateLoading,
|
|
|
|
|
|
aggregateImportDialogOpen,
|
|
|
|
|
|
aggregateImportResultDialogOpen,
|
|
|
|
|
|
aggregateImportPreview,
|
|
|
|
|
|
aggregateImportResult,
|
|
|
|
|
|
aggregateMergeMode,
|
|
|
|
|
|
aggregateMergeModeSelectOpen,
|
|
|
|
|
|
handleExportAggregate,
|
|
|
|
|
|
handleAggregateFileSelect,
|
|
|
|
|
|
confirmImportAggregate,
|
2026-02-14 20:06:38 +08:00
|
|
|
|
} = useConfigExportImport(systemConfig)
|
|
|
|
|
|
|
2026-05-16 02:09:01 +08:00
|
|
|
|
type DataManagementKind = 'config' | 'users' | 'aggregate'
|
|
|
|
|
|
|
|
|
|
|
|
function handleDataExport(kind: DataManagementKind) {
|
|
|
|
|
|
if (kind === 'config') {
|
|
|
|
|
|
handleExportConfig()
|
|
|
|
|
|
} else if (kind === 'users') {
|
|
|
|
|
|
handleExportUsers()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
handleExportAggregate()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDataFileSelect(kind: DataManagementKind, event: Event) {
|
|
|
|
|
|
if (kind === 'config') {
|
|
|
|
|
|
handleConfigFileSelect(event)
|
|
|
|
|
|
} else if (kind === 'users') {
|
|
|
|
|
|
handleUsersFileSelect(event)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
handleAggregateFileSelect(event)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
// Scheduled tasks composable
|
|
|
|
|
|
const {
|
|
|
|
|
|
scheduledTasks,
|
|
|
|
|
|
initPreviousValues,
|
|
|
|
|
|
} = useScheduledTasks(systemConfig)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
2026-01-05 18:18:45 +08:00
|
|
|
|
await Promise.all([
|
|
|
|
|
|
loadSystemConfig(),
|
2026-01-19 03:19:17 +08:00
|
|
|
|
loadSystemVersion(),
|
2026-02-07 14:30:46 +08:00
|
|
|
|
proxyNodesStore.ensureLoaded(),
|
2026-01-05 18:18:45 +08:00
|
|
|
|
])
|
2026-02-14 20:06:38 +08:00
|
|
|
|
// 配置加载完成后初始化定时任务的原始值
|
|
|
|
|
|
initPreviousValues()
|
2026-02-15 18:46:26 +08:00
|
|
|
|
await nextTick()
|
|
|
|
|
|
setupScrollSpy()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
if (observer) {
|
|
|
|
|
|
observer.disconnect()
|
|
|
|
|
|
observer = null
|
|
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
</script>
|