mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +08:00
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(-)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -25,16 +25,14 @@ impl ProxyService {
|
||||
/// 启动代理服务器
|
||||
pub async fn start(&self) -> Result<ProxyServerInfo, String> {
|
||||
// 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!("代理配置已实时应用,无需重启代理服务器");
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 代理目标开关 */}
|
||||
<div
|
||||
className="flex items-center gap-2 ml-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Switch
|
||||
id={`proxy-target-switch-${provider.id}`}
|
||||
checked={provider.isProxyTarget || false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked && !provider.isProxyTarget) {
|
||||
onSetProxyTarget(provider);
|
||||
}
|
||||
}}
|
||||
disabled={provider.isProxyTarget}
|
||||
className="scale-75 data-[state=checked]:bg-purple-500"
|
||||
/>
|
||||
{provider.isProxyTarget && (
|
||||
<Label
|
||||
htmlFor={`proxy-target-switch-${provider.id}`}
|
||||
className="text-xs font-medium text-purple-500 dark:text-purple-400 cursor-pointer"
|
||||
>
|
||||
{t("provider.proxyTarget", { defaultValue: "代理目标" })}
|
||||
</Label>
|
||||
)}
|
||||
{!provider.isProxyTarget && (
|
||||
<Label
|
||||
htmlFor={`proxy-target-switch-${provider.id}`}
|
||||
className="text-xs text-muted-foreground cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
{t("provider.setAsProxyTarget", {
|
||||
defaultValue: "设为代理",
|
||||
})}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
{/* 代理目标开关 - 仅在代理服务运行时显示 */}
|
||||
{isProxyRunning && (
|
||||
<div
|
||||
className="flex items-center gap-2 ml-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Switch
|
||||
id={`proxy-target-switch-${provider.id}`}
|
||||
checked={provider.isProxyTarget || false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked && !provider.isProxyTarget) {
|
||||
onSetProxyTarget(provider);
|
||||
}
|
||||
}}
|
||||
disabled={provider.isProxyTarget}
|
||||
className="scale-75 data-[state=checked]:bg-purple-500"
|
||||
/>
|
||||
{provider.isProxyTarget && (
|
||||
<Label
|
||||
htmlFor={`proxy-target-switch-${provider.id}`}
|
||||
className="text-xs font-medium text-purple-500 dark:text-purple-400 cursor-pointer"
|
||||
>
|
||||
{t("provider.proxyTarget", { defaultValue: "代理目标" })}
|
||||
</Label>
|
||||
)}
|
||||
{!provider.isProxyTarget && (
|
||||
<Label
|
||||
htmlFor={`proxy-target-switch-${provider.id}`}
|
||||
className="text-xs text-muted-foreground cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
{t("provider.setAsProxyTarget", {
|
||||
defaultValue: "设为代理",
|
||||
})}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayUrl && (
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-3">
|
||||
@@ -90,6 +94,7 @@ export function ProviderList({
|
||||
onConfigureUsage={onConfigureUsage}
|
||||
onOpenWebsite={onOpenWebsite}
|
||||
onSetProxyTarget={onSetProxyTarget}
|
||||
isProxyRunning={isProxyRunning}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ProxyConfig, "enabled">;
|
||||
|
||||
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<ProxyConfigForm>({
|
||||
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({
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proxy.settings.basic.description", {
|
||||
defaultValue: "控制代理服务是否启用以及监听的地址与端口。",
|
||||
defaultValue: "配置代理服务监听的地址与端口。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border border-white/10 bg-background/60 p-4">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.enabled.label", {
|
||||
defaultValue: "启用代理服务",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t("proxy.settings.fields.enabled.description", {
|
||||
defaultValue: "开启后可以从命令或 UI 启动代理服务器",
|
||||
})}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
Reference in New Issue
Block a user