From c7ef4365b8e1232f7116ec3b0fbb87728fc6c2a7 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 1 Dec 2025 09:17:12 +0800 Subject: [PATCH] fix(proxy): resolve UI/UX issues and database constraint error Simplify proxy control interface and fix database persistence issues: Backend Fixes: - Fix NOT NULL constraint error in proxy_config.created_at field * Use COALESCE to preserve created_at on updates * Ensure proper INSERT OR REPLACE behavior - Remove redundant enabled field validation on startup * Auto-enable when user clicks start button * Persist enabled state after successful start - Preserve enabled state during config updates * Prevent accidental service shutdown on config save Frontend Improvements: - Remove duplicate proxy enable switch from settings dialog * Keep only runtime toggle in ProxyPanel * Simplify user experience with single control point - Hide proxy target button when proxy service is stopped * Add isProxyRunning prop to ProviderCard * Conditionally render proxy controls based on service status - Update form schema to omit enabled field * Managed automatically by backend Files: 5 changed, 81 insertions(+), 94 deletions(-) --- src-tauri/src/database/dao/proxy.rs | 8 ++- src-tauri/src/services/proxy.rs | 44 ++++++------ src/components/providers/ProviderCard.tsx | 74 +++++++++++--------- src/components/providers/ProviderList.tsx | 8 +++ src/components/proxy/ProxySettingsDialog.tsx | 41 +++-------- 5 files changed, 81 insertions(+), 94 deletions(-) diff --git a/src-tauri/src/database/dao/proxy.rs b/src-tauri/src/database/dao/proxy.rs index 8a91cbea6..187415f8d 100644 --- a/src-tauri/src/database/dao/proxy.rs +++ b/src-tauri/src/database/dao/proxy.rs @@ -50,9 +50,11 @@ impl Database { let conn = lock_conn!(self.conn); conn.execute( - "INSERT OR REPLACE INTO proxy_config - (id, enabled, listen_address, listen_port, max_retries, request_timeout, enable_logging, target_app, updated_at) - VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, datetime('now'))", + "INSERT OR REPLACE INTO proxy_config + (id, enabled, listen_address, listen_port, max_retries, request_timeout, enable_logging, target_app, created_at, updated_at) + VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, + COALESCE((SELECT created_at FROM proxy_config WHERE id = 1), datetime('now')), + datetime('now'))", rusqlite::params![ if config.enabled { 1 } else { 0 }, config.listen_address, diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 8e9e30297..97bf9de3a 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -25,16 +25,14 @@ impl ProxyService { /// 启动代理服务器 pub async fn start(&self) -> Result { // 1. 获取配置 - let config = self + let mut config = self .db .get_proxy_config() .await .map_err(|e| format!("获取代理配置失败: {e}"))?; - // 2. 检查是否启用 - if !config.enabled { - return Err("代理服务未启用,请先在设置中启用".to_string()); - } + // 2. 确保配置启用(用户通过UI启动即表示希望启用) + config.enabled = true; // 3. 检查是否已在运行 if self.server.read().await.is_some() { @@ -42,7 +40,7 @@ impl ProxyService { } // 4. 创建并启动服务器 - let server = ProxyServer::new(config, self.db.clone()); + let server = ProxyServer::new(config.clone(), self.db.clone()); let info = server .start() .await @@ -51,6 +49,12 @@ impl ProxyService { // 5. 保存服务器实例 *self.server.write().await = Some(server); + // 6. 持久化 enabled 状态 + self.db + .update_proxy_config(config) + .await + .map_err(|e| format!("保存代理配置失败: {e}"))?; + log::info!("代理服务器已启动: {}:{}", info.address, info.port); Ok(info) } @@ -99,9 +103,12 @@ impl ProxyService { .await .map_err(|e| format!("获取代理配置失败: {e}"))?; - // 保存到数据库 + // 保存到数据库(保持 enabled 状态不变) + let mut new_config = config.clone(); + new_config.enabled = previous.enabled; + self.db - .update_proxy_config(config.clone()) + .update_proxy_config(new_config.clone()) .await .map_err(|e| format!("保存代理配置失败: {e}"))?; @@ -111,20 +118,9 @@ impl ProxyService { return Ok(()); } - // 如果关闭代理,直接停止 - if !config.enabled { - if let Some(server) = server_guard.take() { - server - .stop() - .await - .map_err(|e| format!("停止代理服务器失败: {e}"))?; - log::info!("代理配置禁用了服务,已自动停止代理服务器"); - } - return Ok(()); - } - - let require_restart = config.listen_address != previous.listen_address - || config.listen_port != previous.listen_port; + // 判断是否需要重启(地址或端口变更) + let require_restart = new_config.listen_address != previous.listen_address + || new_config.listen_port != previous.listen_port; if require_restart { if let Some(server) = server_guard.take() { @@ -134,7 +130,7 @@ impl ProxyService { .map_err(|e| format!("重启前停止代理服务器失败: {e}"))?; } - let new_server = ProxyServer::new(config.clone(), self.db.clone()); + let new_server = ProxyServer::new(new_config, self.db.clone()); new_server .start() .await @@ -143,7 +139,7 @@ impl ProxyService { *server_guard = Some(new_server); log::info!("代理配置已更新,服务器已自动重启应用最新配置"); } else if let Some(server) = server_guard.as_ref() { - server.apply_runtime_config(config).await; + server.apply_runtime_config(&new_config).await; log::info!("代理配置已实时应用,无需重启代理服务器"); } diff --git a/src/components/providers/ProviderCard.tsx b/src/components/providers/ProviderCard.tsx index a3bb8b2a1..8ff23873c 100644 --- a/src/components/providers/ProviderCard.tsx +++ b/src/components/providers/ProviderCard.tsx @@ -31,6 +31,7 @@ interface ProviderCardProps { onOpenWebsite: (url: string) => void; onDuplicate: (provider: Provider) => void; onSetProxyTarget: (provider: Provider) => void; + isProxyRunning: boolean; dragHandleProps?: DragHandleProps; } @@ -80,6 +81,7 @@ export function ProviderCard({ onOpenWebsite, onDuplicate, onSetProxyTarget, + isProxyRunning, dragHandleProps, }: ProviderCardProps) { const { t } = useTranslation(); @@ -169,41 +171,43 @@ export function ProviderCard({ )} - {/* 代理目标开关 */} -
e.stopPropagation()} - > - { - if (checked && !provider.isProxyTarget) { - onSetProxyTarget(provider); - } - }} - disabled={provider.isProxyTarget} - className="scale-75 data-[state=checked]:bg-purple-500" - /> - {provider.isProxyTarget && ( - - )} - {!provider.isProxyTarget && ( - - )} -
+ {/* 代理目标开关 - 仅在代理服务运行时显示 */} + {isProxyRunning && ( +
e.stopPropagation()} + > + { + if (checked && !provider.isProxyTarget) { + onSetProxyTarget(provider); + } + }} + disabled={provider.isProxyTarget} + className="scale-75 data-[state=checked]:bg-purple-500" + /> + {provider.isProxyTarget && ( + + )} + {!provider.isProxyTarget && ( + + )} +
+ )} {displayUrl && ( diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 9d693fff8..e767a9386 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -9,6 +9,7 @@ import type { CSSProperties } from "react"; import type { Provider } from "@/types"; import type { AppId } from "@/lib/api"; import { useDragSort } from "@/hooks/useDragSort"; +import { useProxyStatus } from "@/hooks/useProxyStatus"; import { ProviderCard } from "@/components/providers/ProviderCard"; import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState"; @@ -46,6 +47,9 @@ export function ProviderList({ appId, ); + // 获取代理服务运行状态 + const { isRunning: isProxyRunning } = useProxyStatus(); + if (isLoading) { return (
@@ -90,6 +94,7 @@ export function ProviderList({ onConfigureUsage={onConfigureUsage} onOpenWebsite={onOpenWebsite} onSetProxyTarget={onSetProxyTarget} + isProxyRunning={isProxyRunning} /> ))}
@@ -109,6 +114,7 @@ interface SortableProviderCardProps { onConfigureUsage?: (provider: Provider) => void; onOpenWebsite: (url: string) => void; onSetProxyTarget: (provider: Provider) => void; + isProxyRunning: boolean; } function SortableProviderCard({ @@ -122,6 +128,7 @@ function SortableProviderCard({ onConfigureUsage, onOpenWebsite, onSetProxyTarget, + isProxyRunning, }: SortableProviderCardProps) { const { setNodeRef, @@ -152,6 +159,7 @@ function SortableProviderCard({ } onOpenWebsite={onOpenWebsite} onSetProxyTarget={onSetProxyTarget} + isProxyRunning={isProxyRunning} dragHandleProps={{ attributes, listeners, diff --git a/src/components/proxy/ProxySettingsDialog.tsx b/src/components/proxy/ProxySettingsDialog.tsx index ee26be9b9..572e529b0 100644 --- a/src/components/proxy/ProxySettingsDialog.tsx +++ b/src/components/proxy/ProxySettingsDialog.tsx @@ -26,7 +26,8 @@ import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import type { ProxyConfig } from "@/types/proxy"; -type ProxyConfigForm = ProxyConfig; +// 表单数据类型(不包含 enabled 字段,该字段由后端自动管理) +type ProxyConfigForm = Omit; const createProxyConfigSchema = (t: TFunction) => { const requestTimeoutSchema = z @@ -50,7 +51,6 @@ const createProxyConfigSchema = (t: TFunction) => { }); return z.object({ - enabled: z.boolean(), listen_address: z.string().regex( /^(\d{1,3}\.){3}\d{1,3}$/, t("proxy.settings.validation.addressInvalid", { @@ -108,7 +108,6 @@ export function ProxySettingsDialog({ const form = useForm({ resolver: zodResolver(schema), defaultValues: { - enabled: false, listen_address: "127.0.0.1", listen_port: 5000, max_retries: 3, @@ -128,7 +127,12 @@ export function ProxySettingsDialog({ const onSubmit = async (data: ProxyConfigForm) => { try { - await updateConfig(data); + // 添加 enabled 字段(从当前配置中获取,保持不变) + const configToSave: ProxyConfig = { + ...data, + enabled: config?.enabled ?? true, + }; + await updateConfig(configToSave); closePanel(); } catch (error) { console.error("Save config failed:", error); @@ -196,38 +200,11 @@ export function ProxySettingsDialog({

{t("proxy.settings.basic.description", { - defaultValue: "控制代理服务是否启用以及监听的地址与端口。", + defaultValue: "配置代理服务监听的地址与端口。", })}

- ( - -
- - {t("proxy.settings.fields.enabled.label", { - defaultValue: "启用代理服务", - })} - - - {t("proxy.settings.fields.enabled.description", { - defaultValue: "开启后可以从命令或 UI 启动代理服务器", - })} - -
- - - -
- )} - /> -