mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 08:44:41 +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:
@@ -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