diff --git a/docs/Frpilot开发提示词.md b/docs/Frpilot开发提示词.md index 4afdf26..f3bb066 100644 --- a/docs/Frpilot开发提示词.md +++ b/docs/Frpilot开发提示词.md @@ -35,4 +35,15 @@ - 背景图片使用assets/images目录中的bg-1.png、bg-2.jpg、bg-3.png作为窗口背景图片,在设置页面里面可以选择其中一个切换页面背景,注意尽可能保持原图片的比例大小不发生拉伸 -开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发 \ No newline at end of file +开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发 + +### 3. 功能开发 + +#### 3.1 开发服务器配置功能 + +- Servers.vue页面右上方一个增加和刷新按钮,点击增加,弹窗填写服务器表单信息 +- 服务器信息有名称、IP地址、端口号(默认7000)、备注、排序、是否置顶、是否启用等,这些是我能想到的字段,你需要按照Frpc配置的需要,决定是否增加新的字段,表单需要做必填检验 +- 页面展示已经配置的服务器列表,列表服务器以卡片形式展示,卡片有开关可以直接操作是否置顶和是否启用,是否启用选项只能启用一个,另外还需要有编辑和删除,新增和编辑使用同一个表单 +- 服务器信息在Rust端使用SQLite存储,Rust已经加入相关的sqlx依赖等,后端操作返回前端务必使用ApiResponse统一返回格式,前端也需要做好对应的类型匹配 + +开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件、auto-import和components依赖可以简化引入,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发 \ No newline at end of file diff --git a/src-tauri/src/commands/server.rs b/src-tauri/src/commands/server.rs index 5ab774e..9150598 100644 --- a/src-tauri/src/commands/server.rs +++ b/src-tauri/src/commands/server.rs @@ -2,10 +2,292 @@ use sqlx::SqlitePool; use tauri::State; use tauri_helper::auto_collect_command; -use crate::models::{ApiResponse}; +use crate::models::{ApiResponse, Server, ServerPayload}; +/// 允许的传输协议(对应 transport.protocol) +const PROTOCOLS: [&str; 4] = ["tcp", "kcp", "quic", "websocket"]; + +/// 统一查询字段,使用 concat! 保证结果为 'static str(sqlx 0.9 要求) +macro_rules! select_fields { + () => { + "id, name, addr, port, token, protocol, tls_enable, remark, sort, pinned, enabled, created_at, updated_at" + }; +} + +/// 校验提交数据,返回错误信息(None 表示通过) +fn validate(payload: &ServerPayload) -> Option { + if payload.name.trim().is_empty() { + return Some("服务器名称不能为空".to_string()); + } + if payload.addr.trim().is_empty() { + return Some("服务器地址不能为空".to_string()); + } + if payload.port < 1 || payload.port > 65535 { + return Some("端口号必须在 1 - 65535 之间".to_string()); + } + None +} + +/// 归一化协议,非法值回退为 tcp +fn normalize_protocol(raw: &str) -> String { + let value = raw.trim().to_ascii_lowercase(); + if PROTOCOLS.contains(&value.as_str()) { + value + } else { + "tcp".to_string() + } +} + +/// 空字符串转 None,避免写入无意义的空串 +fn clean(value: &Option) -> Option { + value + .as_ref() + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) +} + +async fn fetch_all(pool: &SqlitePool) -> Result, sqlx::Error> { + sqlx::query_as::<_, Server>(concat!( + "SELECT ", + select_fields!(), + " FROM servers ORDER BY pinned DESC, sort ASC, id ASC" + )) + .fetch_all(pool) + .await +} + +async fn fetch_by_id(pool: &SqlitePool, id: i64) -> Result { + sqlx::query_as::<_, Server>(concat!( + "SELECT ", + select_fields!(), + " FROM servers WHERE id = ?" + )) + .bind(id) + .fetch_one(pool) + .await +} + +/// 取消其它服务器的启用状态(启用互斥) +async fn disable_others<'e, E>(executor: E, id: i64) -> Result<(), sqlx::Error> +where + E: sqlx::Executor<'e, Database = sqlx::Sqlite>, +{ + sqlx::query( + "UPDATE servers SET enabled = 0, updated_at = datetime('now', 'localtime') WHERE enabled = 1 AND id <> ?", + ) + .bind(id) + .execute(executor) + .await?; + Ok(()) +} + +/// 获取服务器列表 #[tauri::command] #[auto_collect_command] -pub async fn get_server_list(state: State<'_, SqlitePool>) -> Result, String> { - Ok(ApiResponse::success_empty()) -} \ No newline at end of file +pub async fn get_server_list( + state: State<'_, SqlitePool>, +) -> Result>, String> { + Ok(match fetch_all(&state).await { + Ok(list) => ApiResponse::success(list), + Err(e) => ApiResponse::error(500, &e.to_string()), + }) +} + +/// 新增服务器 +#[tauri::command] +#[auto_collect_command] +pub async fn create_server( + state: State<'_, SqlitePool>, + payload: ServerPayload, +) -> Result, String> { + if let Some(msg) = validate(&payload) { + return Ok(ApiResponse::error(400, &msg)); + } + + Ok(match insert_server(&state, &payload).await { + Ok(server) => ApiResponse::success(server), + Err(e) => ApiResponse::error(500, &e.to_string()), + }) +} + +async fn insert_server(pool: &SqlitePool, payload: &ServerPayload) -> Result { + let mut tx = pool.begin().await?; + + let result = sqlx::query( + "INSERT INTO servers (name, addr, port, token, protocol, tls_enable, remark, sort, pinned, enabled) \ + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind(payload.name.trim()) + .bind(payload.addr.trim()) + .bind(payload.port) + .bind(clean(&payload.token)) + .bind(normalize_protocol(&payload.protocol)) + .bind(payload.tls_enable) + .bind(clean(&payload.remark)) + .bind(payload.sort) + .bind(payload.pinned) + .bind(payload.enabled) + .execute(&mut *tx) + .await?; + + let id = result.last_insert_rowid(); + + // 若新增时启用了该服务器,则关闭其它服务器的启用状态 + if payload.enabled { + disable_others(&mut *tx, id).await?; + } + + tx.commit().await?; + fetch_by_id(pool, id).await +} + +/// 编辑服务器 +#[tauri::command] +#[auto_collect_command] +pub async fn update_server( + state: State<'_, SqlitePool>, + payload: ServerPayload, +) -> Result, String> { + let Some(id) = payload.id else { + return Ok(ApiResponse::error(400, "缺少服务器 ID")); + }; + if let Some(msg) = validate(&payload) { + return Ok(ApiResponse::error(400, &msg)); + } + + Ok(match update_server_row(&state, id, &payload).await { + Ok(Some(server)) => ApiResponse::success(server), + Ok(None) => ApiResponse::error(404, "服务器不存在"), + Err(e) => ApiResponse::error(500, &e.to_string()), + }) +} + +async fn update_server_row( + pool: &SqlitePool, + id: i64, + payload: &ServerPayload, +) -> Result, sqlx::Error> { + let mut tx = pool.begin().await?; + + let result = sqlx::query( + "UPDATE servers SET name = ?, addr = ?, port = ?, token = ?, protocol = ?, tls_enable = ?, \ + remark = ?, sort = ?, pinned = ?, enabled = ?, updated_at = datetime('now', 'localtime') \ + WHERE id = ?", + ) + .bind(payload.name.trim()) + .bind(payload.addr.trim()) + .bind(payload.port) + .bind(clean(&payload.token)) + .bind(normalize_protocol(&payload.protocol)) + .bind(payload.tls_enable) + .bind(clean(&payload.remark)) + .bind(payload.sort) + .bind(payload.pinned) + .bind(payload.enabled) + .bind(id) + .execute(&mut *tx) + .await?; + + if result.rows_affected() == 0 { + tx.rollback().await?; + return Ok(None); + } + + // 若编辑时启用了该服务器,则关闭其它服务器的启用状态 + if payload.enabled { + disable_others(&mut *tx, id).await?; + } + + tx.commit().await?; + Ok(Some(fetch_by_id(pool, id).await?)) +} + +/// 删除服务器 +#[tauri::command] +#[auto_collect_command] +pub async fn delete_server( + state: State<'_, SqlitePool>, + id: i64, +) -> Result, String> { + Ok( + match sqlx::query("DELETE FROM servers WHERE id = ?") + .bind(id) + .execute(&*state) + .await + { + Ok(result) if result.rows_affected() == 0 => ApiResponse::error(404, "服务器不存在"), + Ok(_) => ApiResponse::success_empty(), + Err(e) => ApiResponse::error(500, &e.to_string()), + }, + ) +} + +/// 启用 / 禁用服务器(启用时自动关闭其它服务器) +#[tauri::command] +#[auto_collect_command] +pub async fn set_server_enabled( + state: State<'_, SqlitePool>, + id: i64, + enabled: bool, +) -> Result, String> { + Ok(match toggle_enabled(&state, id, enabled).await { + Ok(Some(server)) => ApiResponse::success(server), + Ok(None) => ApiResponse::error(404, "服务器不存在"), + Err(e) => ApiResponse::error(500, &e.to_string()), + }) +} + +async fn toggle_enabled( + pool: &SqlitePool, + id: i64, + enabled: bool, +) -> Result, sqlx::Error> { + let mut tx = pool.begin().await?; + + let result = sqlx::query( + "UPDATE servers SET enabled = ?, updated_at = datetime('now', 'localtime') WHERE id = ?", + ) + .bind(enabled) + .bind(id) + .execute(&mut *tx) + .await?; + + if result.rows_affected() == 0 { + tx.rollback().await?; + return Ok(None); + } + + if enabled { + disable_others(&mut *tx, id).await?; + } + + tx.commit().await?; + Ok(Some(fetch_by_id(pool, id).await?)) +} + +/// 置顶 / 取消置顶服务器 +#[tauri::command] +#[auto_collect_command] +pub async fn set_server_pinned( + state: State<'_, SqlitePool>, + id: i64, + pinned: bool, +) -> Result, String> { + Ok( + match sqlx::query( + "UPDATE servers SET pinned = ?, updated_at = datetime('now', 'localtime') WHERE id = ?", + ) + .bind(pinned) + .bind(id) + .execute(&*state) + .await + { + Ok(result) if result.rows_affected() == 0 => ApiResponse::error(404, "服务器不存在"), + Ok(_) => match fetch_by_id(&state, id).await { + Ok(server) => ApiResponse::success(server), + Err(e) => ApiResponse::error(500, &e.to_string()), + }, + Err(e) => ApiResponse::error(500, &e.to_string()), + }, + ) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 461ca97..09a08af 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,7 +4,10 @@ mod services; use tauri::Manager; -use crate::commands::server::get_server_list; +use crate::commands::server::{ + create_server, delete_server, get_server_list, set_server_enabled, set_server_pinned, + update_server, +}; use crate::services::db::init_pool; #[cfg_attr(mobile, tauri::mobile_entry_point)] diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 6d74489..11e7ed7 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use sqlx::FromRow; #[derive(Debug, Serialize, Deserialize)] pub struct PageResult { @@ -48,3 +49,49 @@ impl ApiResponse<()> { } } } + +/// 服务器配置(对应 SQLite 中的 servers 表) +#[derive(Debug, Serialize, Deserialize, FromRow)] +pub struct Server { + pub id: i64, + /// 名称(仅用于界面展示) + pub name: String, + /// 服务器地址,对应 frpc 的 serverAddr,可为 IP 或域名 + pub addr: String, + /// 服务器端口,对应 frpc 的 serverPort,默认 7000 + pub port: i32, + /// 鉴权 Token,对应 auth.token + pub token: Option, + /// 传输协议,对应 transport.protocol:tcp / kcp / quic / websocket + pub protocol: String, + /// 是否启用 TLS,对应 transport.tls.enable + pub tls_enable: bool, + /// 备注 + pub remark: Option, + /// 排序(越小越靠前) + pub sort: i32, + /// 是否置顶 + pub pinned: bool, + /// 是否启用(全局同时只能启用一个) + pub enabled: bool, + pub created_at: String, + pub updated_at: String, +} + +/// 新增 / 编辑服务器的提交数据 +#[derive(Debug, Serialize, Deserialize)] +pub struct ServerPayload { + /// 编辑时携带,新增时为空 + pub id: Option, + pub name: String, + pub addr: String, + pub port: i32, + pub token: Option, + pub protocol: String, + pub tls_enable: bool, + pub remark: Option, + pub sort: i32, + pub pinned: bool, + pub enabled: bool, +} + diff --git a/src-tauri/src/services/db.rs b/src-tauri/src/services/db.rs index 8a5db82..86d4bb1 100644 --- a/src-tauri/src/services/db.rs +++ b/src-tauri/src/services/db.rs @@ -16,5 +16,28 @@ pub async fn init_pool(app: &AppHandle) -> Result { .connect_with(options) .await?; + // 初始化表结构 + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS servers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + addr TEXT NOT NULL, + port INTEGER NOT NULL DEFAULT 7000, + token TEXT, + protocol TEXT NOT NULL DEFAULT 'tcp', + tls_enable INTEGER NOT NULL DEFAULT 0, + remark TEXT, + sort INTEGER NOT NULL DEFAULT 0, + pinned INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), + updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) + ) + "#, + ) + .execute(&pool) + .await?; + Ok(pool) } diff --git a/src/api/server.ts b/src/api/server.ts new file mode 100644 index 0000000..c6b0c75 --- /dev/null +++ b/src/api/server.ts @@ -0,0 +1,46 @@ +import { invoke } from "@tauri-apps/api/core" +import type { ApiResponse, Server, ServerPayload } from "@/types/server" + +/** + * 调用后端命令并统一解包 ApiResponse。 + * + * 后端所有服务器相关命令均返回 `ApiResponse`,业务失败时 + * `success === false`,此处统一抛出异常,交由调用方提示。 + */ +async function call(command: string, args?: Record): Promise { + const res = await invoke>(command, args) + if (!res?.success) { + throw new Error(res?.msg || "操作失败") + } + return res.data as T +} + +/** 获取服务器列表 */ +export function getServerList() { + return call("get_server_list") +} + +/** 新增服务器 */ +export function createServer(payload: ServerPayload) { + return call("create_server", { payload }) +} + +/** 编辑服务器 */ +export function updateServer(payload: ServerPayload) { + return call("update_server", { payload }) +} + +/** 删除服务器 */ +export function deleteServer(id: number) { + return call("delete_server", { id }) +} + +/** 启用 / 禁用服务器(启用时后端会自动关闭其它服务器) */ +export function setServerEnabled(id: number, enabled: boolean) { + return call("set_server_enabled", { id, enabled }) +} + +/** 置顶 / 取消置顶服务器 */ +export function setServerPinned(id: number, pinned: boolean) { + return call("set_server_pinned", { id, pinned }) +} diff --git a/src/assets/main.css b/src/assets/main.css index fa211d2..1008dfd 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -49,6 +49,13 @@ body { background-color: transparent; } +/* Antd 按钮:图标与文字按 flex 垂直居中(Tailwind preflight 会把 svg 设为 block,导致行内对齐偏移) */ +.ant-btn { + display: inline-flex !important; + align-items: center; + justify-content: center; +} + /* 窗口圆角固定 8px,与 Windows 11 系统默认圆角保持一致 */ #app { border-radius: 8px; diff --git a/src/components/server/ServerCard.vue b/src/components/server/ServerCard.vue new file mode 100644 index 0000000..ddabc7f --- /dev/null +++ b/src/components/server/ServerCard.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/server/ServerFormModal.vue b/src/components/server/ServerFormModal.vue new file mode 100644 index 0000000..4ea2aae --- /dev/null +++ b/src/components/server/ServerFormModal.vue @@ -0,0 +1,261 @@ + + + diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 16279ba..b4b1051 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -54,7 +54,44 @@ "bg3": "Background 3" } }, + "server": { + "refresh": "Refresh", + "add": "Add server", + "empty": "No servers yet. Click Add in the top right corner.", + "edit": "Edit", + "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete server \"{name}\"?", + "name": "Name", + "namePlaceholder": "Enter a server name", + "addr": "Server address", + "addrPlaceholder": "IP or domain, e.g. 127.0.0.1 / frp.example.com", + "port": "Port", + "token": "Auth token", + "tokenPlaceholder": "Corresponds to frpc auth.token, optional", + "protocol": "Transport protocol", + "tls": "Enable TLS", + "remark": "Remark", + "remarkPlaceholder": "Optional remark", + "sort": "Sort", + "pinned": "Pinned", + "enabled": "Enabled", + "enabledTip": "Only one server can be enabled at a time", + "saveSuccess": "Saved successfully", + "deleteSuccess": "Deleted successfully", + "form": { + "addTitle": "Add server", + "editTitle": "Edit server", + "nameRequired": "Please enter the server name", + "addrRequired": "Please enter the server address", + "addrInvalid": "Please enter a valid IP address or domain", + "portRequired": "Please enter the port", + "portRange": "Port must be between 1 and 65535" + } + }, "common": { - "developing": "This feature is under construction" + "developing": "This feature is under construction", + "confirm": "OK", + "cancel": "Cancel", + "save": "Save" } } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 27fb09f..26e85a9 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -54,7 +54,44 @@ "bg3": "背景三" } }, + "server": { + "refresh": "刷新", + "add": "新增服务器", + "empty": "暂无服务器,点击右上角新增", + "edit": "编辑", + "delete": "删除", + "deleteConfirm": "确定要删除服务器「{name}」吗?", + "name": "名称", + "namePlaceholder": "请输入服务器名称", + "addr": "服务器地址", + "addrPlaceholder": "IP 地址或域名,如 127.0.0.1 / frp.example.com", + "port": "端口", + "token": "鉴权 Token", + "tokenPlaceholder": "对应 frpc 的 auth.token,可留空", + "protocol": "传输协议", + "tls": "启用 TLS", + "remark": "备注", + "remarkPlaceholder": "可选备注信息", + "sort": "排序", + "pinned": "置顶", + "enabled": "启用", + "enabledTip": "同时只能启用一个服务器", + "saveSuccess": "保存成功", + "deleteSuccess": "删除成功", + "form": { + "addTitle": "新增服务器", + "editTitle": "编辑服务器", + "nameRequired": "请输入服务器名称", + "addrRequired": "请输入服务器地址", + "addrInvalid": "请输入合法的 IP 地址或域名", + "portRequired": "请输入端口号", + "portRange": "端口号必须在 1 - 65535 之间" + } + }, "common": { - "developing": "功能开发中,敬请期待" + "developing": "功能开发中,敬请期待", + "confirm": "确定", + "cancel": "取消", + "save": "保存" } } diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 20d0302..1b1708a 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -13,17 +13,36 @@ declare module 'vue' { export interface GlobalComponents { AButton: typeof import('ant-design-vue/es')['Button'] ACard: typeof import('ant-design-vue/es')['Card'] + ACol: typeof import('ant-design-vue/es')['Col'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] ADivider: typeof import('ant-design-vue/es')['Divider'] ADropdown: typeof import('ant-design-vue/es')['Dropdown'] + AEmpty: typeof import('ant-design-vue/es')['Empty'] + AForm: typeof import('ant-design-vue/es')['Form'] + AFormItem: typeof import('ant-design-vue/es')['FormItem'] + AInput: typeof import('ant-design-vue/es')['Input'] + AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] + AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AMenu: typeof import('ant-design-vue/es')['Menu'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] + AModal: typeof import('ant-design-vue/es')['Modal'] + APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] AProgress: typeof import('ant-design-vue/es')['Progress'] AResult: typeof import('ant-design-vue/es')['Result'] + ARow: typeof import('ant-design-vue/es')['Row'] ASegmented: typeof import('ant-design-vue/es')['Segmented'] + ASelect: typeof import('ant-design-vue/es')['Select'] + ASpace: typeof import('ant-design-vue/es')['Space'] + ASpin: typeof import('ant-design-vue/es')['Spin'] + ASwitch: typeof import('ant-design-vue/es')['Switch'] + ATag: typeof import('ant-design-vue/es')['Tag'] + ATextarea: typeof import('ant-design-vue/es')['Textarea'] + ATooltip: typeof import('ant-design-vue/es')['Tooltip'] MacTitleBar: typeof import('./../components/MacTitleBar.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + ServerCard: typeof import('./../components/server/ServerCard.vue')['default'] + ServerFormModal: typeof import('./../components/server/ServerFormModal.vue')['default'] TitleBarActions: typeof import('./../components/TitleBarActions.vue')['default'] WindowsTitleBar: typeof import('./../components/WindowsTitleBar.vue')['default'] } diff --git a/src/types/server.ts b/src/types/server.ts new file mode 100644 index 0000000..407bf14 --- /dev/null +++ b/src/types/server.ts @@ -0,0 +1,65 @@ +/** + * 服务器相关的类型定义,字段与 Rust 端 `models::Server` / `models::ServerPayload` 一一对应。 + */ + +/** 统一响应结构,对应 Rust 端 `models::ApiResponse` */ +export interface ApiResponse { + success: boolean + code: number + msg: string + data: T | null +} + +/** 传输协议,对应 frpc 的 transport.protocol */ +export type ServerProtocol = "tcp" | "kcp" | "quic" | "websocket" + +/** 服务器配置 */ +export interface Server { + id: number + /** 名称(仅用于界面展示) */ + name: string + /** 服务器地址 serverAddr,IP 或域名 */ + addr: string + /** 服务器端口 serverPort */ + port: number + /** 鉴权 Token auth.token */ + token: string | null + /** 传输协议 transport.protocol */ + protocol: ServerProtocol + /** 是否启用 TLS transport.tls.enable */ + tls_enable: boolean + /** 备注 */ + remark: string | null + /** 排序(越小越靠前) */ + sort: number + /** 是否置顶 */ + pinned: boolean + /** 是否启用(全局同时只能启用一个) */ + enabled: boolean + created_at: string + updated_at: string +} + +/** 新增 / 编辑服务器提交数据 */ +export interface ServerPayload { + /** 编辑时携带,新增时省略 */ + id?: number + name: string + addr: string + port: number + token?: string | null + protocol: ServerProtocol + tls_enable: boolean + remark?: string | null + sort: number + pinned: boolean + enabled: boolean +} + +/** 协议下拉选项 */ +export const SERVER_PROTOCOLS: { value: ServerProtocol; label: string }[] = [ + { value: "tcp", label: "TCP" }, + { value: "kcp", label: "KCP" }, + { value: "quic", label: "QUIC" }, + { value: "websocket", label: "WebSocket" }, +] diff --git a/src/views/Servers.vue b/src/views/Servers.vue index 8552bbd..f864128 100644 --- a/src/views/Servers.vue +++ b/src/views/Servers.vue @@ -1,12 +1,117 @@