diff --git a/composables/guiConfig.ts b/composables/guiConfig.ts new file mode 100644 index 0000000..6399492 --- /dev/null +++ b/composables/guiConfig.ts @@ -0,0 +1,132 @@ +import { BaseDirectory, exists, mkdir, readDir, readTextFile, remove, writeTextFile } from "@tauri-apps/plugin-fs"; +import type { RoomProfile } from "@/stores/index"; + +const GUI_CONFIG_VERSION = 1; + +type RoomGuiConfigFile = { + version: number; + savedAt: string; + roomOrder?: number; + room: RoomProfile; +}; + +const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; + +const isRoomGuiConfigFile = (value: unknown): value is RoomGuiConfigFile => { + if (!isRecord(value) || value.version !== GUI_CONFIG_VERSION || !isRecord(value.room)) { + return false; + } + const room = value.room; + return ( + typeof value.savedAt === "string" && + Number.isFinite(Date.parse(value.savedAt)) && + (value.roomOrder === undefined || (typeof value.roomOrder === "number" && Number.isInteger(value.roomOrder))) && + typeof room.id === "string" && + Boolean(room.id) && + typeof room.name === "string" && + typeof room.rpcPortal === "number" && + isRecord(room.config) + ); +}; + +const ensureGuiConfigDirectory = async () => { + const directoryPath = import.meta.env.VITE_GUI_CONFIG_PATH; + if (!(await exists(directoryPath, { baseDir: BaseDirectory.Resource }))) { + await mkdir(directoryPath, { baseDir: BaseDirectory.Resource, recursive: true }); + } + return directoryPath; +}; + +const writeRoomGuiConfig = async (directoryPath: string, room: RoomProfile, roomOrder: number) => { + const filePath = `${directoryPath}${room.id}.json`; + const content = JSON.stringify( + { + version: GUI_CONFIG_VERSION, + savedAt: new Date().toISOString(), + roomOrder, + room + }, + null, + 4 + ); + await writeTextFile(filePath, content, { baseDir: BaseDirectory.Resource }); + return filePath; +}; + +export const saveRoomGuiConfig = async (room: RoomProfile, roomOrder: number) => { + const directoryPath = await ensureGuiConfigDirectory(); + return writeRoomGuiConfig(directoryPath, room, roomOrder); +}; + +export const saveRoomGuiConfigs = async (rooms: RoomProfile[]) => { + const directoryPath = await ensureGuiConfigDirectory(); + const writtenFilePaths: string[] = []; + try { + for (const [index, room] of rooms.entries()) { + const filePath = `${directoryPath}${room.id}.json`; + try { + writtenFilePaths.push(await writeRoomGuiConfig(directoryPath, room, index)); + } catch (error) { + writtenFilePaths.push(filePath); + throw error; + } + } + return writtenFilePaths; + } catch (error) { + // 首次迁移必须全量成功,避免下次启动把不完整目录误判为完整的多房间数据源。 + await Promise.allSettled( + writtenFilePaths.map(filePath => remove(filePath, { baseDir: BaseDirectory.Resource })) + ); + throw error; + } +}; + +export const loadRoomGuiConfigs = async () => { + const directoryPath = import.meta.env.VITE_GUI_CONFIG_PATH; + if (!(await exists(directoryPath, { baseDir: BaseDirectory.Resource }))) { + return [] as RoomProfile[]; + } + + const entries = await readDir(directoryPath, { baseDir: BaseDirectory.Resource }); + const configs = await Promise.all( + entries + .filter(entry => entry.isFile && entry.name.toLowerCase().endsWith(".json")) + .map(async entry => { + const filePath = `${directoryPath}${entry.name}`; + try { + const parsedConfig: unknown = JSON.parse(await readTextFile(filePath, { baseDir: BaseDirectory.Resource })); + if (!isRoomGuiConfigFile(parsedConfig)) { + throw new Error("文件结构或版本不受支持"); + } + return parsedConfig; + } catch (error) { + console.error(`读取 GUI 房间配置“${entry.name}”失败`, error); + return null; + } + }) + ); + + const roomConfigById = new Map(); + for (const config of configs) { + if (!config) continue; + const savedConfig = roomConfigById.get(config.room.id); + if (!savedConfig || Date.parse(config.savedAt) >= Date.parse(savedConfig.savedAt)) { + roomConfigById.set(config.room.id, config); + } + } + + return [...roomConfigById.values()] + .sort( + (a, b) => + (a.roomOrder ?? Number.MAX_SAFE_INTEGER) - (b.roomOrder ?? Number.MAX_SAFE_INTEGER) || + a.room.name.localeCompare(b.room.name, "zh-CN") + ) + .map(config => config.room); +}; + +export const removeRoomGuiConfig = async (roomId: string) => { + const filePath = `${import.meta.env.VITE_GUI_CONFIG_PATH}${roomId}.json`; + if (await exists(filePath, { baseDir: BaseDirectory.Resource })) { + await remove(filePath, { baseDir: BaseDirectory.Resource }); + } +}; diff --git a/composables/windows.ts b/composables/windows.ts index a4f5501..c41ea38 100644 --- a/composables/windows.ts +++ b/composables/windows.ts @@ -8,6 +8,11 @@ import { updateConfigJsonBounce, type ConfigServerUrlType } from "./configJson"; const _listenersMaps: { [key: string]: [UnlistenFn | null] } = {}; +export const MAIN_WINDOW_LOGICAL_SIZE = Object.freeze({ + width: 340, + height: 350 +}); + const dealCloseListener = async (dialog: WebviewWindow, label: string, beforeCloseFunc?: () => void | null) => { const unlistenFnList: [UnlistenFn | null] = _listenersMaps[label] || [null]; let [unListenlogClose] = unlistenFnList; @@ -44,13 +49,12 @@ export default async ( defaultOpts.parent = appWindow; // console.error(logicalPosition); if (defaultOpts.x == 0 && defaultOpts.y == 0) { - const logicalAppSize = { - width: 340, - height: 305 - } const factor = await appWindow.scaleFactor(); const appPosition = await appWindow.outerPosition(); - const logicalPosition = new PhysicalPosition(appPosition.x + Math.ceil((logicalAppSize.width + 5) * factor), appPosition.y).toLogical(factor); + const logicalPosition = new PhysicalPosition( + appPosition.x + Math.ceil((MAIN_WINDOW_LOGICAL_SIZE.width + 5) * factor), + appPosition.y + ).toLogical(factor); defaultOpts.x = logicalPosition.x; defaultOpts.y = logicalPosition.y; } diff --git a/env/.env.dev b/env/.env.dev index 74122f0..94df9e7 100644 --- a/env/.env.dev +++ b/env/.env.dev @@ -1,6 +1,7 @@ VITE_CONFIG_PATH=easytier/config/ VITE_CONFIG_ADMIN_PATH=easytier/config/.admin/ VITE_CONFIG_FILE_NAME=easytier/config.json +VITE_GUI_CONFIG_PATH=easytier/gui_config/ VITE_LOG_PATH=easytier/logs/ VITE_CACHE_PATH=easytier/cache/ VITE_GAME_LIST_PATH=easytier/gameList/ diff --git a/env/.env.prod b/env/.env.prod index 74122f0..94df9e7 100644 --- a/env/.env.prod +++ b/env/.env.prod @@ -1,6 +1,7 @@ VITE_CONFIG_PATH=easytier/config/ VITE_CONFIG_ADMIN_PATH=easytier/config/.admin/ VITE_CONFIG_FILE_NAME=easytier/config.json +VITE_GUI_CONFIG_PATH=easytier/gui_config/ VITE_LOG_PATH=easytier/logs/ VITE_CACHE_PATH=easytier/cache/ VITE_GAME_LIST_PATH=easytier/gameList/ diff --git a/pages/cidr.vue b/pages/cidr.vue index 52a3f69..f3dddfd 100644 --- a/pages/cidr.vue +++ b/pages/cidr.vue @@ -54,9 +54,12 @@ import { dataSubscribe } from "@/composables/windows"; import { invoke } from "@tauri-apps/api/core"; import { reactive, onMounted, onBeforeUnmount } from "vue"; - import { ATJ, parseCliInfo } from "@/utils"; + import { ATJ } from "@/utils"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; const mainStore = useMainStore(); + if (!mainStore.activeRoomProfile) { + mainStore.initializeRoomProfiles(); + } const data = reactive<{ [key: string]: any }>({ route: [], @@ -67,7 +70,13 @@ const listenOutput = async () => { data.isRunning = true; - const [error, member] = await ATJ(invoke("get_route_by_cli")); + const rpcPortal = mainStore.activeRoomProfile?.rpcPortal; + if (!rpcPortal) { + data.isRunning = false; + data.route = []; + return; + } + const [error, member] = await ATJ(invoke("get_route_by_cli", { rpc_portal: rpcPortal })); data.isRunning = false; if(!member) return; if (error) { @@ -76,10 +85,14 @@ } if (member === "_EasytierGameCliFailedToConnect_") { data.route = []; - await listenOutput(); return; } - data.route = JSON.parse(member); + try { + data.route = JSON.parse(member); + } catch (error) { + console.error("解析当前房间的子网代理路由失败", error); + data.route = []; + } }; const listenStart = async () => { diff --git a/pages/index.vue b/pages/index.vue index 8442318..6bfd614 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,4 +1,76 @@