From 4046140413031b9bfb9c2f2396f5fb77e304861b Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sat, 11 Oct 2025 12:22:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B8=AD=E8=BD=AC=E7=AB=99?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8F=AF=E6=8B=96=E6=8B=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/relay_stations.rs | 52 +++- src-tauri/src/main.rs | 3 +- src/components/RelayStationManager.tsx | 313 +++++++---------------- src/components/SortableStationItem.tsx | 248 ++++++++++++++++++ src/lib/api.ts | 15 ++ 5 files changed, 405 insertions(+), 226 deletions(-) create mode 100644 src/components/SortableStationItem.tsx diff --git a/src-tauri/src/commands/relay_stations.rs b/src-tauri/src/commands/relay_stations.rs index 8cdbde9..69d6e89 100644 --- a/src-tauri/src/commands/relay_stations.rs +++ b/src-tauri/src/commands/relay_stations.rs @@ -57,6 +57,7 @@ pub struct RelayStation { pub user_id: Option, // 用户 ID(可选) pub adapter_config: Option>, // 适配器特定配置 pub enabled: bool, // 启用状态 + pub display_order: i32, // 显示顺序 pub created_at: i64, // 创建时间 pub updated_at: i64, // 更新时间 } @@ -180,6 +181,7 @@ impl RelayStation { user_id: row.get("user_id")?, adapter_config, enabled: row.get::<_, i32>("enabled")? == 1, + display_order: row.get::<_, i32>("display_order").unwrap_or(0), created_at: row.get("created_at")?, updated_at: row.get("updated_at")?, }) @@ -202,6 +204,7 @@ pub fn init_relay_stations_tables(conn: &Connection) -> Result<()> { user_id TEXT, adapter_config TEXT, enabled INTEGER NOT NULL DEFAULT 1, + display_order INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) @@ -244,7 +247,13 @@ pub async fn relay_stations_list(db: State<'_, AgentDb>) -> Result, + db: State<'_, AgentDb> +) -> Result<(), String> { + let conn = db.0.lock().map_err(|e| { + log::error!("Failed to acquire database lock: {}", e); + i18n::t("database.lock_failed") + })?; + + // 开始事务 + let tx = conn.unchecked_transaction().map_err(|e| { + log::error!("Failed to start transaction: {}", e); + i18n::t("database.transaction_failed") + })?; + + // 更新每个中转站的排序 + for (index, station_id) in station_ids.iter().enumerate() { + tx.execute( + "UPDATE relay_stations SET display_order = ?1, updated_at = ?2 WHERE id = ?3", + params![index as i32, Utc::now().timestamp(), station_id], + ).map_err(|e| { + log::error!("Failed to update station order: {}", e); + i18n::t("database.update_failed") + })?; + } + + // 提交事务 + tx.commit().map_err(|e| { + log::error!("Failed to commit transaction: {}", e); + i18n::t("database.transaction_failed") + })?; + + log::info!("Updated display order for {} relay stations", station_ids.len()); + Ok(()) } \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 26287b1..10b27b5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -59,7 +59,7 @@ use commands::relay_stations::{ relay_stations_list, relay_station_get, relay_station_create, relay_station_update, relay_station_delete, relay_station_toggle_enable, relay_station_sync_config, relay_station_restore_config, relay_station_get_current_config, - relay_stations_export, relay_stations_import, + relay_stations_export, relay_stations_import, relay_station_update_order, }; use commands::relay_adapters::{ relay_station_get_info, relay_station_get_user_info, @@ -452,6 +452,7 @@ fn main() { relay_station_get_current_config, relay_stations_export, relay_stations_import, + relay_station_update_order, relay_station_get_info, relay_station_get_user_info, relay_station_test_connection, diff --git a/src/components/RelayStationManager.tsx b/src/components/RelayStationManager.tsx index 61d3ea9..a8ab327 100644 --- a/src/components/RelayStationManager.tsx +++ b/src/components/RelayStationManager.tsx @@ -1,9 +1,8 @@ import React, { useState, useEffect } from 'react'; import { open } from '@tauri-apps/plugin-shell'; import MonacoEditor from '@monaco-editor/react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { @@ -34,9 +33,6 @@ import { } from '@/lib/api'; import { Plus, - Edit, - Trash2, - Globe, Server, ArrowLeft, Settings, @@ -49,6 +45,22 @@ import { Download, Upload } from 'lucide-react'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { SortableStationItem } from './SortableStationItem'; interface RelayStationManagerProps { onBack: () => void; @@ -83,6 +95,38 @@ const RelayStationManager: React.FC = ({ onBack }) => const { t } = useTranslation(); + // 拖拽传感器配置 + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + // 拖拽结束处理 + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = stations.findIndex(station => station.id === active.id); + const newIndex = stations.findIndex(station => station.id === over.id); + + if (oldIndex !== -1 && newIndex !== -1) { + const newStations = arrayMove(stations, oldIndex, newIndex); + setStations(newStations); + + try { + await api.relayStationUpdateOrder(newStations.map(s => s.id)); + showToast('排序已更新', 'success'); + } catch (error) { + console.error('Failed to update station order:', error); + showToast('更新排序失败', 'error'); + setStations(stations); + } + } + } + }; + // Token 脱敏函数 const maskToken = (token: string): string => { if (!token || token.length <= 8) { @@ -665,226 +709,47 @@ const RelayStationManager: React.FC = ({ onBack }) => {/* 中转站列表 */} -
- {loading ? ( -
-
-

{t('common.loading')}

+ + s.id)} + strategy={verticalListSortingStrategy} + > +
+ {loading ? ( +
+
+

{t('common.loading')}

+
+ ) : stations.length === 0 ? ( +
+ +

{t('relayStation.noStations')}

+

{t('relayStation.noStationsDesc')}

+ +
+ ) : ( + stations.map((station) => ) + )}
- ) : stations.length === 0 ? ( -
- -

{t('relayStation.noStations')}

-

{t('relayStation.noStationsDesc')}

- -
- ) : ( - stations.map((station) => ( - - -
-
- {station.name} - - {getAdapterDisplayName(station.adapter)} - -
-
- {getStatusBadge(station)} - - -
-
-
- -
-
- - {station.api_url} -
- - {station.description && ( -

- {station.description} -

- )} - - {/* PackyCode 额度显示 */} - {station.adapter === 'packycode' && ( -
- {loadingQuota[station.id] ? ( -
-
- 加载中... -
- ) : quotaData[station.id] ? ( -
- {/* 用户信息和计划 */} -
-
- {quotaData[station.id].username && ( - {quotaData[station.id].username} - )} - - {quotaData[station.id].plan_type.toUpperCase()} - - {quotaData[station.id].opus_enabled && ( - - Opus - - )} -
-
- - {/* 账户余额 */} -
- 余额: - - ${Number(quotaData[station.id].balance_usd).toFixed(2)} - -
- - {/* 日额度 */} -
-
- 日额度: -
- {(() => { - const daily_spent = Number(quotaData[station.id].daily_spent_usd); - const daily_budget = Number(quotaData[station.id].daily_budget_usd); - return ( - <> - daily_budget * 0.8 ? 'text-orange-600' : 'text-green-600'}> - ${daily_spent.toFixed(2)} - - / ${daily_budget.toFixed(2)} - - ); - })()} -
-
-
-
{ - const daily_spent = Number(quotaData[station.id].daily_spent_usd); - const daily_budget = Number(quotaData[station.id].daily_budget_usd); - return daily_spent / daily_budget > 0.8; - })() ? 'bg-orange-500' : 'bg-green-500' - }`} - style={{ width: `${Math.min( - (() => { - const daily_spent = Number(quotaData[station.id].daily_spent_usd); - const daily_budget = Number(quotaData[station.id].daily_budget_usd); - return (daily_spent / daily_budget) * 100; - })(), 100)}%` }} - /> -
-
- - {/* 月额度 */} -
-
- 月额度: -
- {(() => { - const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); - const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); - return ( - <> - monthly_budget * 0.8 ? 'text-orange-600' : 'text-green-600'}> - ${monthly_spent.toFixed(2)} - - / - ${monthly_budget.toFixed(2)} - - ); - })()} -
-
-
-
{ - const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); - const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); - return monthly_spent / monthly_budget > 0.8; - })() ? 'bg-orange-500' : 'bg-green-500' - }`} - style={{ width: `${Math.min( - (() => { - const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); - const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); - return (monthly_spent / monthly_budget) * 100; - })(), 100)}%` }} - /> -
-
- - {/* 总消费 */} -
- 总消费: ${Number(quotaData[station.id].total_spent_usd).toFixed(2)} - -
-
- ) : ( -
- -
- )} -
- )} - -
- - - )) - )} -
+ + {/* 编辑对话框 */} {selectedStation && ( diff --git a/src/components/SortableStationItem.tsx b/src/components/SortableStationItem.tsx new file mode 100644 index 0000000..e361633 --- /dev/null +++ b/src/components/SortableStationItem.tsx @@ -0,0 +1,248 @@ +import React from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Edit, + Trash2, + Globe, + GripVertical, +} from 'lucide-react'; +import { + RelayStation, + RelayStationAdapter, + PackycodeUserQuota, +} from '@/lib/api'; + +interface SortableStationItemProps { + station: RelayStation; + getStatusBadge: (station: RelayStation) => React.ReactNode; + getAdapterDisplayName: (adapter: RelayStationAdapter) => string; + setSelectedStation: (station: RelayStation) => void; + setShowEditDialog: (show: boolean) => void; + openDeleteDialog: (station: RelayStation) => void; + quotaData: Record; + loadingQuota: Record; +} + +/** + * 可排序的中转站卡片组件 + * @author yovinchen + */ +export const SortableStationItem: React.FC = ({ + station, + getStatusBadge, + getAdapterDisplayName, + setSelectedStation, + setShowEditDialog, + openDeleteDialog, + quotaData, + loadingQuota, +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: station.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( + + +
+
+ +
+ {station.name} + + {getAdapterDisplayName(station.adapter)} + +
+
+
+ {getStatusBadge(station)} + + +
+
+
+ +
+
+ + {station.api_url} +
+ + {station.description && ( +

+ {station.description} +

+ )} + + {/* PackyCode 额度显示 */} + {station.adapter === 'packycode' && ( +
+ {loadingQuota[station.id] ? ( +
+
+ 加载中... +
+ ) : quotaData[station.id] ? ( +
+ {/* 用户信息和计划 */} +
+
+ {quotaData[station.id].username && ( + {quotaData[station.id].username} + )} + + {quotaData[station.id].plan_type.toUpperCase()} + + {quotaData[station.id].opus_enabled && ( + + Opus + + )} +
+
+ + {/* 账户余额 */} +
+ 余额: + + ${Number(quotaData[station.id].balance_usd).toFixed(2)} + +
+ + {/* 日额度 */} +
+
+ 日额度: +
+ {(() => { + const daily_spent = Number(quotaData[station.id].daily_spent_usd); + const daily_budget = Number(quotaData[station.id].daily_budget_usd); + return ( + <> + daily_budget * 0.8 ? 'text-orange-600' : 'text-green-600'}> + ${daily_spent.toFixed(2)} + + / ${daily_budget.toFixed(2)} + + ); + })()} +
+
+
+
{ + const daily_spent = Number(quotaData[station.id].daily_spent_usd); + const daily_budget = Number(quotaData[station.id].daily_budget_usd); + return daily_spent / daily_budget > 0.8; + })() ? 'bg-orange-500' : 'bg-green-500' + }`} + style={{ width: `${Math.min( + (() => { + const daily_spent = Number(quotaData[station.id].daily_spent_usd); + const daily_budget = Number(quotaData[station.id].daily_budget_usd); + return (daily_spent / daily_budget) * 100; + })(), 100)}%` }} + /> +
+
+ + {/* 月额度 */} +
+
+ 月额度: +
+ {(() => { + const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); + const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); + return ( + <> + monthly_budget * 0.8 ? 'text-orange-600' : 'text-green-600'}> + ${monthly_spent.toFixed(2)} + + / + ${monthly_budget.toFixed(2)} + + ); + })()} +
+
+
+
{ + const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); + const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); + return monthly_spent / monthly_budget > 0.8; + })() ? 'bg-orange-500' : 'bg-green-500' + }`} + style={{ width: `${Math.min( + (() => { + const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); + const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); + return (monthly_spent / monthly_budget) * 100; + })(), 100)}%` }} + /> +
+
+ + {/* 总消费 */} +
+ 总消费: + ${Number(quotaData[station.id].total_spent_usd).toFixed(2)} +
+
+ ) : ( +
+ 额度信息加载失败 +
+ )} +
+ )} +
+ + + ); +}; diff --git a/src/lib/api.ts b/src/lib/api.ts index 8403d99..9576512 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2478,6 +2478,21 @@ export const api = { } }, + /** + * Updates the display order of relay stations + * @author yovinchen + * @param stationIds - Array of station IDs in the new order + * @returns Promise resolving when order is updated + */ + async relayStationUpdateOrder(stationIds: string[]): Promise { + try { + return await invoke("relay_station_update_order", { stationIds }); + } catch (error) { + console.error("Failed to update relay station order:", error); + throw error; + } + }, + // ============= PackyCode Nodes ============= /**