diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f798fbc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+dist-electron
+release
+*.local
+
+# Editor directories and files
+.vscode/.debug.env
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# lockfile
+package-lock.json
+pnpm-lock.yaml
+yarn.lock
\ No newline at end of file
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000..16bb32c
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ bracketSpacing: true,
+ singleQuote: false,
+ arrowParens: "avoid",
+ trailingComma: "none"
+};
diff --git a/LICENSE b/LICENSE
index 1cbdade..8d3fda8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2023 8473136
+Copyright (c) 2023 刘嘉伟
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/electron-builder.json5 b/electron-builder.json5
new file mode 100644
index 0000000..5ba2076
--- /dev/null
+++ b/electron-builder.json5
@@ -0,0 +1,46 @@
+/**
+ * @see https://www.electron.build/configuration/configuration
+ */
+{
+ "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
+ "appId": "priv.liujiawei.frpc.desktop",
+ "asar": true,
+ "productName": "Frpc-Desktop",
+ "directories": {
+ "output": "release/${version}"
+ },
+ "icon": "./public/logo/512x512.png",
+ "files": [
+ "dist",
+ "dist-electron"
+ ],
+ "mac": {
+ "target": [
+ "dmg"
+ ],
+ "artifactName": "${productName}-Mac-${version}-Installer.${ext}"
+ },
+ "win": {
+ "target": [
+ {
+ "target": "nsis",
+ "arch": [
+ "x64"
+ ]
+ }
+ ],
+ "artifactName": "${productName}-Windows-${version}-Setup.${ext}"
+ },
+ "nsis": {
+ "oneClick": false,
+ "perMachine": false,
+ "allowToChangeInstallationDirectory": true,
+ "deleteAppDataOnUninstall": false
+ },
+ "linux": {
+ "target": [
+ "AppImage"
+ ],
+ "artifactName": "${productName}-Linux-${version}.${ext}"
+ },
+}
diff --git a/electron/api/config.ts b/electron/api/config.ts
new file mode 100644
index 0000000..44cb054
--- /dev/null
+++ b/electron/api/config.ts
@@ -0,0 +1,42 @@
+import { ipcMain } from "electron";
+import { getConfig, saveConfig } from "../storage/config";
+import { listVersion } from "../storage/version";
+
+export const initConfigApi = () => {
+ ipcMain.on("config.saveConfig", async (event, args) => {
+ saveConfig(args, (err, numberOfUpdated, upsert) => {
+ event.reply("Config.saveConfig.hook", {
+ err: err,
+ numberOfUpdated: numberOfUpdated,
+ upsert: upsert
+ });
+ });
+ });
+
+ ipcMain.on("config.getConfig", async (event, args) => {
+ getConfig((err, doc) => {
+ event.reply("Config.getConfig.hook", {
+ err: err,
+ data: doc
+ });
+ });
+ });
+
+ ipcMain.on("config.versions", event => {
+ listVersion((err, doc) => {
+ event.reply("Config.versions.hook", {
+ err: err,
+ data: doc
+ });
+ });
+ });
+
+ ipcMain.on("config.hasConfig", event => {
+ getConfig((err, doc) => {
+ event.reply("Config.getConfig.hook", {
+ err: err,
+ data: doc
+ });
+ });
+ });
+};
diff --git a/electron/api/frpc.ts b/electron/api/frpc.ts
new file mode 100644
index 0000000..95c547f
--- /dev/null
+++ b/electron/api/frpc.ts
@@ -0,0 +1,183 @@
+import { app, ipcMain } from "electron";
+import { Config, getConfig } from "../storage/config";
+import { listProxy } from "../storage/proxy";
+import { getVersionById } from "../storage/version";
+
+const fs = require("fs");
+const path = require("path");
+
+const { exec, spawn } = require("child_process");
+export let frpcProcess = null;
+
+const runningCmd = {
+ commandPath: null,
+ configPath: null
+};
+
+// const getFrpc = (config: Config) => {
+// getVersionById(config.currentVersion, (err, document) => {
+// if (!err) {
+// }
+// });
+// };
+
+/**
+ * 获取选择版本的工作目录
+ * @param versionId 版本ID
+ * @param callback
+ */
+const getFrpcVersionWorkerPath = (
+ versionId: string,
+ callback: (workerPath: string) => void
+) => {
+ getVersionById(versionId, (err2, version) => {
+ if (!err2) {
+ callback(version["frpcVersionPath"]);
+ }
+ });
+};
+
+/**
+ * 生成配置文件
+ */
+export const generateConfig = (
+ config: Config,
+ callback: (configPath: string) => void
+) => {
+ listProxy((err3, proxys) => {
+ if (!err3) {
+ const proxyToml = proxys.map(m => {
+ let toml = `
+[[proxies]]
+name = "${m.name}"
+type = "${m.type}"
+localIP = "${m.localIp}"
+localPort = ${m.localPort}
+`;
+ switch (m.type) {
+ case "tcp":
+ toml += `remotePort = ${m.remotePort}`;
+ break;
+ case "http":
+ case "https":
+ toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]`;
+ break;
+ default:
+ break;
+ }
+
+ return toml;
+ });
+ let toml = `
+serverAddr = "${config.serverAddr}"
+serverPort = ${config.serverPort}
+auth.method = "${config.authMethod}"
+auth.token = "${config.authToken}"
+log.to = "frpc.log"
+log.level = "debug"
+log.maxDays = 3
+webServer.addr = "127.0.0.1"
+webServer.port = 57400
+
+${proxyToml}
+ `;
+
+ // const configPath = path.join("frp.toml");
+ const filename = "frp.toml";
+ fs.writeFile(
+ path.join(app.getPath("userData"), filename), // 配置文件目录
+ toml, // 配置文件内容
+ { flag: "w" },
+ err => {
+ if (!err) {
+ callback(filename);
+ }
+ }
+ );
+ }
+ });
+};
+
+/**
+ * 启动frpc子进程
+ * @param cwd
+ * @param commandPath
+ * @param configPath
+ */
+const startFrpcProcess = (commandPath: string, configPath: string) => {
+ const command = `${commandPath} -c ${configPath}`;
+ frpcProcess = spawn(command, {
+ cwd: app.getPath("userData"),
+ shell: true
+ });
+ runningCmd.commandPath = commandPath;
+ runningCmd.configPath = configPath;
+ frpcProcess.stdout.on("data", data => {
+ console.log(`命令输出: ${data}`);
+ });
+ frpcProcess.stdout.on("error", data => {
+ console.log(`执行错误: ${data}`);
+ frpcProcess.kill("SIGINT");
+ });
+};
+
+export const reloadFrpcProcess = () => {
+ if (frpcProcess && !frpcProcess.killed) {
+ getConfig((err1, config) => {
+ if (!err1) {
+ if (config) {
+ generateConfig(config, configPath => {
+ const command = `${runningCmd.commandPath} reload -c ${configPath}`;
+ console.log("重启", command);
+ exec(command, {
+ cwd: app.getPath("userData"),
+ shell: true
+ });
+ });
+ }
+ }
+ });
+ }
+};
+
+export const initFrpcApi = () => {
+ ipcMain.handle("frpc.running", async (event, args) => {
+ if (!frpcProcess) {
+ return false;
+ } else {
+ return !frpcProcess.killed;
+ }
+ });
+
+ ipcMain.on("frpc.start", async (event, args) => {
+ getConfig((err1, config) => {
+ if (!err1) {
+ if (config) {
+ getFrpcVersionWorkerPath(
+ config.currentVersion,
+ (frpcVersionPath: string) => {
+ generateConfig(config, configPath => {
+ startFrpcProcess(
+ path.join(frpcVersionPath, "frpc"),
+ configPath
+ );
+ });
+ }
+ );
+ } else {
+ event.reply(
+ "Home.frpc.start.error.hook",
+ "请先前往设置页面,修改配置后再启动"
+ );
+ }
+ }
+ });
+ });
+
+ ipcMain.on("frpc.stop", () => {
+ if (frpcProcess && !frpcProcess.killed) {
+ console.log("关闭");
+ frpcProcess.kill();
+ }
+ });
+};
diff --git a/electron/api/github.ts b/electron/api/github.ts
new file mode 100644
index 0000000..fe896ae
--- /dev/null
+++ b/electron/api/github.ts
@@ -0,0 +1,121 @@
+import { app, BrowserWindow, ipcMain, net } from "electron";
+import { insertVersion } from "../storage/version";
+
+const fs = require("fs");
+const path = require("path");
+const zlib = require("zlib");
+const { download } = require("electron-dl");
+
+const unTarGZ = tarGzPath => {
+ const tar = require("tar");
+ const targetPath = path.resolve(path.join(app.getPath("userData"), "frp"));
+ const unzip = zlib.createGunzip();
+ const readStream = fs.createReadStream(tarGzPath);
+ if (!fs.existsSync(unzip)) {
+ fs.mkdirSync(targetPath, { recursive: true });
+ }
+ readStream.pipe(unzip).pipe(
+ tar.extract({
+ cwd: targetPath,
+ filter: filePath => path.basename(filePath) === "frpc"
+ })
+ );
+ return path.join("frp", path.basename(tarGzPath, ".tar.gz"));
+ // .on("finish", () => {
+ // console.log("解压完成!");
+ // });
+};
+export const initGitHubApi = () => {
+ // 版本
+ let versions = [];
+
+ const getVersion = versionId => {
+ return versions.find(f => f.id === versionId);
+ };
+
+ const getAdaptiveAsset = versionId => {
+ const version = getVersion(versionId);
+ const arch = process.arch;
+ const platform = process.platform;
+ const { assets } = version;
+ const asset = assets.find(
+ f => f.name.indexOf(platform) != -1 && f.name.indexOf(arch) != -1
+ );
+ return asset;
+ };
+
+ /**
+ * 获取github上的frp所有版本
+ */
+ ipcMain.on("github.getFrpVersions", async event => {
+ const request = net.request({
+ method: "get",
+ url: "https://api.github.com/repos/fatedier/frp/releases"
+ });
+ request.on("response", response => {
+ let responseData: Buffer = Buffer.alloc(0);
+ response.on("data", (data: Buffer) => {
+ responseData = Buffer.concat([responseData, data]);
+ });
+ response.on("end", () => {
+ versions = JSON.parse(responseData.toString());
+ // const borderContent: Electron.WebContents =
+ // BrowserWindow.getFocusedWindow().webContents;
+ const downloadPath = path.join(app.getPath("userData"), "download");
+ const returnVersionsData = versions
+ .filter(f => getAdaptiveAsset(f.id))
+ .map(m => {
+ const asset = getAdaptiveAsset(m.id);
+ if (asset) {
+ // const absPath = `${downloadPath}/${asset.id}_${asset.name}`;
+ const absPath = `${downloadPath}/${asset.name}`;
+ m.download_completed = fs.existsSync(absPath);
+ } else {
+ console.log("buzhicih");
+ }
+ // m.download_completed = fs.existsSync(
+ // `${downloadPath}/${asset.id}_${asset.name}`
+ // );
+ return m;
+ });
+ event.reply("Download.frpVersionHook", returnVersionsData);
+ });
+ });
+ request.end();
+ });
+
+ /**
+ * 下载请求
+ */
+ ipcMain.on("github.download", async (event, args) => {
+ const version = getVersion(args);
+ const asset = getAdaptiveAsset(args);
+ const { browser_download_url } = asset;
+ // 数据目录
+ await download(BrowserWindow.getFocusedWindow(), browser_download_url, {
+ filename: `${asset.name}`,
+ directory: path.join(app.getPath("userData"), "download"),
+ onProgress: progress => {
+ event.reply("Download.frpVersionDownloadOnProgress", {
+ id: args,
+ progress: progress
+ });
+ },
+ onCompleted: () => {
+ const frpcVersionPath = unTarGZ(
+ path.join(
+ path.join(app.getPath("userData"), "download"),
+ `${asset.name}`
+ )
+ );
+ version["frpcVersionPath"] = frpcVersionPath;
+ insertVersion(version, (err, document) => {
+ if (!err) {
+ event.reply("Download.frpVersionDownloadOnCompleted", args);
+ version.download_completed = true;
+ }
+ });
+ }
+ });
+ });
+};
diff --git a/electron/api/logger.ts b/electron/api/logger.ts
new file mode 100644
index 0000000..f76f024
--- /dev/null
+++ b/electron/api/logger.ts
@@ -0,0 +1,30 @@
+import { app, ipcMain } from "electron";
+
+const fs = require("fs");
+const path = require("path");
+export const initLoggerApi = () => {
+ const logPath = path.join(app.getPath("userData"), "frpc.log");
+
+ const readLogger = (callback: (content: string) => void) => {
+ fs.readFile(logPath, "utf-8", (error, data) => {
+ if (!error) {
+ callback(data);
+ }
+ });
+ };
+
+ ipcMain.on("logger.getLog", async (event, args) => {
+ readLogger(content => {
+ event.reply("Logger.getLog.hook", content);
+ });
+ });
+ ipcMain.on("logger.update", (event, args) => {
+ fs.watch(logPath, (eventType, filename) => {
+ if (eventType === "change") {
+ readLogger(content => {
+ event.reply("Logger.update.hook", content);
+ });
+ }
+ });
+ });
+};
diff --git a/electron/api/proxy.ts b/electron/api/proxy.ts
new file mode 100644
index 0000000..2ded76c
--- /dev/null
+++ b/electron/api/proxy.ts
@@ -0,0 +1,69 @@
+import { ipcMain } from "electron";
+import {
+ deleteProxyById,
+ getProxyById,
+ insertProxy,
+ listProxy,
+ updateProxyById
+} from "../storage/proxy";
+import { reloadFrpcProcess } from "./frpc";
+
+
+
+export const initProxyApi = () => {
+ ipcMain.on("proxy.getProxys", async (event, args) => {
+ listProxy((err, documents) => {
+ event.reply("Proxy.getProxys.hook", {
+ err: err,
+ data: documents
+ });
+ });
+ });
+
+ ipcMain.on("proxy.insertProxy", async (event, args) => {
+ delete args["_id"];
+ insertProxy(args, (err, documents) => {
+ if (!err) {
+ reloadFrpcProcess()
+ }
+ event.reply("Proxy.insertProxy.hook", {
+ err: err,
+ data: documents
+ });
+ });
+ });
+
+ ipcMain.on("proxy.deleteProxyById", async (event, args) => {
+ deleteProxyById(args, (err, documents) => {
+ if (!err) {
+ reloadFrpcProcess()
+ }
+ event.reply("Proxy.deleteProxyById.hook", {
+ err: err,
+ data: documents
+ });
+ });
+ });
+
+ ipcMain.on("proxy.getProxyById", async (event, args) => {
+ getProxyById(args, (err, documents) => {
+ event.reply("Proxy.getProxyById.hook", {
+ err: err,
+ data: documents
+ });
+ });
+ });
+
+ ipcMain.on("proxy.updateProxy", async (event, args) => {
+ if (!args._id) return;
+ updateProxyById(args, (err, documents) => {
+ if (!err) {
+ reloadFrpcProcess()
+ }
+ event.reply("Proxy.updateProxy.hook", {
+ err: err,
+ data: documents
+ });
+ });
+ });
+};
diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts
new file mode 100644
index 0000000..b4c4211
--- /dev/null
+++ b/electron/electron-env.d.ts
@@ -0,0 +1,11 @@
+///
+
+declare namespace NodeJS {
+ interface ProcessEnv {
+ VSCODE_DEBUG?: 'true'
+ DIST_ELECTRON: string
+ DIST: string
+ /** /dist/ or /public/ */
+ VITE_PUBLIC: string
+ }
+}
diff --git a/electron/main/index.ts b/electron/main/index.ts
new file mode 100644
index 0000000..a0f03cc
--- /dev/null
+++ b/electron/main/index.ts
@@ -0,0 +1,136 @@
+import { app, BrowserWindow, ipcMain, shell } from "electron";
+import { release } from "node:os";
+import { join } from "node:path";
+import { initGitHubApi } from "../api/github";
+import { initConfigApi } from "../api/config";
+import { initProxyApi } from "../api/proxy";
+import { initFrpcApi } from "../api/frpc";
+import { initLoggerApi } from "../api/logger";
+// The built directory structure
+//
+// ├─┬ dist-electron
+// │ ├─┬ main
+// │ │ └── index.js > Electron-Main
+// │ └─┬ preload
+// │ └── index.js > Preload-Scripts
+// ├─┬ dist
+// │ └── index.html > Electron-Renderer
+//
+process.env.DIST_ELECTRON = join(__dirname, "..");
+process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
+process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
+ ? join(process.env.DIST_ELECTRON, "../public")
+ : process.env.DIST;
+
+// Disable GPU Acceleration for Windows 7
+if (release().startsWith("6.1")) app.disableHardwareAcceleration();
+
+// Set application name for Windows 10+ notifications
+if (process.platform === "win32") app.setAppUserModelId(app.getName());
+
+if (!app.requestSingleInstanceLock()) {
+ app.quit();
+ process.exit(0);
+}
+
+// Remove electron security warnings
+// This warning only shows in development mode
+// Read more on https://www.electronjs.org/docs/latest/tutorial/security
+// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
+
+let win: BrowserWindow | null = null;
+// Here, you can also use other preload
+const preload = join(__dirname, "../preload/index.js");
+const url = process.env.VITE_DEV_SERVER_URL;
+const indexHtml = join(process.env.DIST, "index.html");
+
+async function createWindow() {
+ win = new BrowserWindow({
+ title: "Main window",
+ icon: join(process.env.VITE_PUBLIC, "favicon.ico"),
+ webPreferences: {
+ preload,
+ // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
+ // Consider using contextBridge.exposeInMainWorld
+ // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
+ nodeIntegration: true,
+ contextIsolation: false
+ }
+ });
+
+ if (process.env.VITE_DEV_SERVER_URL) {
+ // electron-vite-vue#298
+ win.loadURL(url);
+ // Open devTool if the app is not packaged
+ win.webContents.openDevTools();
+ } else {
+ win.loadFile(indexHtml);
+ }
+
+ // Test actively push message to the Electron-Renderer
+ win.webContents.on("did-finish-load", () => {
+ win?.webContents.send("main-process-message", new Date().toLocaleString());
+ });
+
+ // Make all links open with the browser, not with the application
+ win.webContents.setWindowOpenHandler(({ url }) => {
+ if (url.startsWith("https:")) shell.openExternal(url);
+ return { action: "deny" };
+ });
+
+ // 隐藏菜单栏
+ const { Menu } = require("electron");
+ Menu.setApplicationMenu(null);
+ // hide menu for Mac
+ if (process.platform !== "darwin") {
+ app.dock.hide();
+ }
+ // win.webContents.on('will-navigate', (event, url) => { }) #344
+}
+
+app.whenReady().then(createWindow);
+
+app.on("window-all-closed", () => {
+ win = null;
+ if (process.platform !== "darwin") app.quit();
+});
+
+app.on("second-instance", () => {
+ if (win) {
+ // Focus on the main window if the user tried to open another
+ if (win.isMinimized()) win.restore();
+ win.focus();
+ }
+});
+
+app.on("activate", () => {
+ const allWindows = BrowserWindow.getAllWindows();
+ if (allWindows.length) {
+ allWindows[0].focus();
+ } else {
+ createWindow();
+ }
+});
+
+// New window example arg: new windows url
+ipcMain.handle("open-win", (_, arg) => {
+ const childWindow = new BrowserWindow({
+ webPreferences: {
+ preload,
+ nodeIntegration: true,
+ contextIsolation: false
+ }
+ });
+
+ if (process.env.VITE_DEV_SERVER_URL) {
+ childWindow.loadURL(`${url}#${arg}`);
+ } else {
+ childWindow.loadFile(indexHtml, { hash: arg });
+ }
+});
+
+initGitHubApi();
+initConfigApi();
+initProxyApi();
+initFrpcApi();
+initLoggerApi();
diff --git a/electron/main/index111.ts b/electron/main/index111.ts
new file mode 100644
index 0000000..71dd9b7
--- /dev/null
+++ b/electron/main/index111.ts
@@ -0,0 +1,127 @@
+import { app, BrowserWindow, ipcMain, shell } from "electron";
+import { release } from "node:os";
+import { join } from "node:path";
+import { initGitHubApi } from "../api/github";
+import { initConfigApi } from "../api/config";
+import { initProxyApi } from "../api/proxy";
+import { initFrpcApi } from "../api/frpc";
+import { initLoggerApi } from "../api/logger";
+
+// The built directory structure
+//
+// ├─┬ dist-electron
+// │ ├─┬ main
+// │ │ └── index.js > Electron-Main
+// │ └─┬ preload
+// │ └── index.js > Preload-Scripts
+// ├─┬ dist
+// │ └── index.html > Electron-Renderer
+//
+process.env.DIST_ELECTRON = join(__dirname, "..");
+process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
+process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
+ ? join(process.env.DIST_ELECTRON, "../public")
+ : process.env.DIST;
+
+// Disable GPU Acceleration for Windows 7
+if (release().startsWith("6.1")) app.disableHardwareAcceleration();
+
+// Set application name for Windows 10+ notifications
+if (process.platform === "win32") app.setAppUserModelId(app.getName());
+
+if (!app.requestSingleInstanceLock()) {
+ app.quit();
+ process.exit(0);
+}
+
+// Remove electron security warnings
+// This warning only shows in development mode
+// Read more on https://www.electronjs.org/docs/latest/tutorial/security
+// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
+
+let win: BrowserWindow | null = null;
+// Here, you can also use other preload
+const preload = join(__dirname, '../preload/index.js')
+const url = process.env.VITE_DEV_SERVER_URL;
+const indexHtml = join(process.env.DIST, "index.html");
+
+async function createWindow() {
+ win = new BrowserWindow({
+ width: 800,
+ height: 600,
+ resizable: false,
+ title: "Frpc Desktop",
+ icon: join(process.env.VITE_PUBLIC, "favicon.ico"),
+ webPreferences: {
+ preload,
+ // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
+ // Consider using contextBridge.exposeInMainWorld
+ // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
+ nodeIntegration: true,
+ contextIsolation: false
+ }
+ });
+
+ if (process.env.VITE_DEV_SERVER_URL) {
+ // electron-vite-vue#298
+ win.loadURL(url);
+ // Open devTool if the app is not packaged
+ win.webContents.openDevTools();
+ } else {
+ win.loadFile(indexHtml);
+ }
+
+ // Make all links open with the browser, not with the application
+ win.webContents.setWindowOpenHandler(({ url }) => {
+ if (url.startsWith("https:")) shell.openExternal(url);
+ return { action: "deny" };
+ });
+ // win.webContents.on('will-navigate', (event, url) => { }) #344
+}
+
+app.whenReady().then(createWindow);
+
+app.on("window-all-closed", () => {
+ win = null;
+ if (process.platform !== "darwin") app.quit();
+});
+
+app.on("second-instance", () => {
+ if (win) {
+ // Focus on the main window if the user tried to open another
+ if (win.isMinimized()) win.restore();
+ win.focus();
+ }
+});
+
+app.on("activate", () => {
+ const allWindows = BrowserWindow.getAllWindows();
+ if (allWindows.length) {
+ allWindows[0].focus();
+ } else {
+ createWindow();
+ }
+});
+
+// New window example arg: new windows url
+ipcMain.handle('open-win', (_, arg) => {
+ const childWindow = new BrowserWindow({
+ webPreferences: {
+ preload,
+ nodeIntegration: true,
+ contextIsolation: false,
+ },
+ })
+
+ if (process.env.VITE_DEV_SERVER_URL) {
+ childWindow.loadURL(`${url}#${arg}`)
+ } else {
+ childWindow.loadFile(indexHtml, { hash: arg })
+ }
+})
+
+initGitHubApi();
+initConfigApi();
+initProxyApi();
+initFrpcApi();
+initLoggerApi();
diff --git a/electron/preload/index.ts b/electron/preload/index.ts
new file mode 100644
index 0000000..ebf1276
--- /dev/null
+++ b/electron/preload/index.ts
@@ -0,0 +1,92 @@
+function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
+ return new Promise((resolve) => {
+ if (condition.includes(document.readyState)) {
+ resolve(true)
+ } else {
+ document.addEventListener('readystatechange', () => {
+ if (condition.includes(document.readyState)) {
+ resolve(true)
+ }
+ })
+ }
+ })
+}
+
+const safeDOM = {
+ append(parent: HTMLElement, child: HTMLElement) {
+ if (!Array.from(parent.children).find(e => e === child)) {
+ return parent.appendChild(child)
+ }
+ },
+ remove(parent: HTMLElement, child: HTMLElement) {
+ if (Array.from(parent.children).find(e => e === child)) {
+ return parent.removeChild(child)
+ }
+ },
+}
+
+/**
+ * https://tobiasahlin.com/spinkit
+ * https://connoratherton.com/loaders
+ * https://projects.lukehaas.me/css-loaders
+ * https://matejkustec.github.io/SpinThatShit
+ */
+function useLoading() {
+ const className = `loaders-css__square-spin`
+ const styleContent = `
+@keyframes square-spin {
+ 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
+ 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
+ 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
+ 100% { transform: perspective(100px) rotateX(0) rotateY(0); }
+}
+.${className} > div {
+ animation-fill-mode: both;
+ width: 50px;
+ height: 50px;
+ background: #fff;
+ animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
+}
+.app-loading-wrap {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #282c34;
+ z-index: 9;
+}
+ `
+ const oStyle = document.createElement('style')
+ const oDiv = document.createElement('div')
+
+ oStyle.id = 'app-loading-style'
+ oStyle.innerHTML = styleContent
+ oDiv.className = 'app-loading-wrap'
+ oDiv.innerHTML = `
`
+
+ return {
+ appendLoading() {
+ safeDOM.append(document.head, oStyle)
+ safeDOM.append(document.body, oDiv)
+ },
+ removeLoading() {
+ safeDOM.remove(document.head, oStyle)
+ safeDOM.remove(document.body, oDiv)
+ },
+ }
+}
+
+// ----------------------------------------------------------------------
+
+const { appendLoading, removeLoading } = useLoading()
+domReady().then(appendLoading)
+
+window.onmessage = (ev) => {
+ ev.data.payload === 'removeLoading' && removeLoading()
+}
+
+setTimeout(removeLoading, 4999)
diff --git a/electron/storage/config.ts b/electron/storage/config.ts
new file mode 100644
index 0000000..625e427
--- /dev/null
+++ b/electron/storage/config.ts
@@ -0,0 +1,37 @@
+import Datastore from "nedb";
+import path from "path";
+import { app } from "electron";
+
+const configDB = new Datastore({
+ autoload: true,
+ filename: path.join(app.getPath("userData"), "config.db")
+});
+
+export type Config = {
+ currentVersion: any;
+ serverAddr: string;
+ serverPort: number;
+ authMethod: string;
+ authToken: string;
+};
+
+/**
+ * 保存
+ */
+export const saveConfig = (
+ document: Config,
+ cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
+) => {
+ document["_id"] = "1";
+ configDB.update({ _id: "1" }, document, { upsert: true }, cb);
+};
+
+/**
+ * 查找
+ * @param cb
+ */
+export const getConfig = (
+ cb: (err: Error | null, document: Config) => void
+) => {
+ configDB.findOne({ _id: "1" }, cb);
+};
diff --git a/electron/storage/proxy.ts b/electron/storage/proxy.ts
new file mode 100644
index 0000000..4b85855
--- /dev/null
+++ b/electron/storage/proxy.ts
@@ -0,0 +1,74 @@
+import Datastore from "nedb";
+import path from "path";
+import { app } from "electron";
+
+const proxyDB = new Datastore({
+ autoload: true,
+ filename: path.join(app.getPath("userData"), "proxy.db")
+});
+
+export type Proxy = {
+ _id: string;
+ name: string;
+ type: string;
+ localIp: string;
+ localPort: number;
+ remotePort: number;
+ customDomains: string[];
+};
+/**
+ * 新增代理
+ * @param proxy
+ * @param cb
+ */
+export const insertProxy = (
+ proxy: Proxy,
+ cb?: (err: Error | null, document: Proxy) => void
+) => {
+ console.log("新增", proxy);
+ proxyDB.insert(proxy, cb);
+};
+
+/**
+ * 删除代理
+ * @param _id
+ * @param cb
+ */
+export const deleteProxyById = (
+ _id: string,
+ cb?: (err: Error | null, n: number) => void
+) => {
+ proxyDB.remove({ _id: _id }, cb);
+};
+
+/**
+ * 修改代理
+ */
+export const updateProxyById = (
+ proxy: Proxy,
+ cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
+) => {
+ proxyDB.update({ _id: proxy._id }, proxy, {}, cb);
+};
+
+/**
+ * 查找
+ * @param cb
+ */
+export const listProxy = (
+ callback: (err: Error | null, documents: Proxy[]) => void
+) => {
+ proxyDB.find({}, callback);
+};
+
+/**
+ * 根据id查询
+ * @param id
+ * @param callback
+ */
+export const getProxyById = (
+ id: string,
+ callback: (err: Error | null, document: Proxy) => void
+) => {
+ proxyDB.findOne({ _id: id }, callback);
+};
diff --git a/electron/storage/version.ts b/electron/storage/version.ts
new file mode 100644
index 0000000..ce9a80c
--- /dev/null
+++ b/electron/storage/version.ts
@@ -0,0 +1,38 @@
+import Datastore from "nedb";
+import path from "path";
+import { Proxy } from "./proxy";
+import { app } from "electron";
+
+const versionDB = new Datastore({
+ autoload: true,
+ filename: path.join(app.getPath("userData"), "version.db")
+});
+
+/**
+ * 新增代理
+ * @param proxy
+ * @param cb
+ */
+export const insertVersion = (
+ version: any,
+ cb?: (err: Error | null, document: any) => void
+) => {
+ versionDB.insert(version, cb);
+};
+
+/**
+ * 查找
+ * @param cb
+ */
+export const listVersion = (
+ callback: (err: Error | null, documents: any[]) => void
+) => {
+ versionDB.find({}, callback);
+};
+
+export const getVersionById = (
+ id: string,
+ callback: (err: Error | null, document: any) => void
+) => {
+ versionDB.findOne({ id: id }, callback);
+};
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d1364fd
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Frpc Desktop
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..71d4896
--- /dev/null
+++ b/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "Frpc-Desktop",
+ "version": "1.0.0",
+ "main": "dist-electron/main/index.js",
+ "description": "一个frpc桌面客户端",
+ "author": "刘嘉伟 <8473136@qq.com>",
+ "license": "MIT",
+ "private": true,
+ "keywords": [
+ "electron",
+ "rollup",
+ "vite",
+ "vue3",
+ "vue"
+ ],
+ "debug": {
+ "env": {
+ "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/"
+ }
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit && vite build && electron-builder",
+ "build:win": "vue-tsc --noEmit && vite build && electron-builder -w",
+ "preview": "vite preview",
+ "electron:generate-icons": "electron-icon-builder --input=./public/logo.png --output=build --flatten"
+ },
+ "devDependencies": {
+ "@iconify/vue": "^4.1.1",
+ "@types/nedb": "^1.8.16",
+ "@vitejs/plugin-vue": "^4.3.3",
+ "@vue/eslint-config-prettier": "^7.1.0",
+ "@vueuse/core": "^9.13.0",
+ "autoprefixer": "^10.4.15",
+ "cssnano": "^6.0.1",
+ "electron": "^26.0.0",
+ "electron-builder": "^24.6.3",
+ "element-plus": "^2.4.2",
+ "eslint-plugin-prettier": "^4.2.1",
+ "moment": "^2.29.4",
+ "nedb": "^1.8.0",
+ "node-cmd": "^5.0.0",
+ "prettier": "^2.8.8",
+ "sass": "^1.66.1",
+ "sass-loader": "^13.3.2",
+ "tailwindcss": "^3.3.3",
+ "tree-kill": "^1.2.2",
+ "typescript": "^5.1.6",
+ "vite": "^4.4.9",
+ "vite-plugin-electron": "^0.15.3",
+ "vite-plugin-electron-renderer": "^0.14.5",
+ "vue-tsc": "^1.8.8",
+ "vue-types": "^5.1.1",
+ "electron-icon-builder": "^2.0.1",
+ "vue": "^3.3.4",
+ "vue-router": "^4.2.4"
+ },
+ "dependencies": {
+ "electron-dl": "^3.5.1",
+ "tar": "^6.2.0"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..20d42d8
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ plugins: {
+ "postcss-import": {},
+ tailwindcss: {},
+ autoprefixer: {},
+ ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
+ }
+};
diff --git a/public/logo/1024x1024.png b/public/logo/1024x1024.png
new file mode 100644
index 0000000..f60b506
Binary files /dev/null and b/public/logo/1024x1024.png differ
diff --git a/public/logo/128x128.png b/public/logo/128x128.png
new file mode 100644
index 0000000..2c1df6b
Binary files /dev/null and b/public/logo/128x128.png differ
diff --git a/public/logo/16x16.png b/public/logo/16x16.png
new file mode 100644
index 0000000..0941120
Binary files /dev/null and b/public/logo/16x16.png differ
diff --git a/public/logo/24x24.png b/public/logo/24x24.png
new file mode 100644
index 0000000..ac5941c
Binary files /dev/null and b/public/logo/24x24.png differ
diff --git a/public/logo/256x256.png b/public/logo/256x256.png
new file mode 100644
index 0000000..f66aac8
Binary files /dev/null and b/public/logo/256x256.png differ
diff --git a/public/logo/32x32.png b/public/logo/32x32.png
new file mode 100644
index 0000000..3500143
Binary files /dev/null and b/public/logo/32x32.png differ
diff --git a/public/logo/48x48.png b/public/logo/48x48.png
new file mode 100644
index 0000000..ca4cdbd
Binary files /dev/null and b/public/logo/48x48.png differ
diff --git a/public/logo/512x512.png b/public/logo/512x512.png
new file mode 100644
index 0000000..027756d
Binary files /dev/null and b/public/logo/512x512.png differ
diff --git a/public/logo/64x64.png b/public/logo/64x64.png
new file mode 100644
index 0000000..4d31016
Binary files /dev/null and b/public/logo/64x64.png differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..080019b
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/src/assets/logo.png b/src/assets/logo.png
new file mode 100644
index 0000000..1f6665c
Binary files /dev/null and b/src/assets/logo.png differ
diff --git a/src/layout/compoenets/AppMain.vue b/src/layout/compoenets/AppMain.vue
new file mode 100644
index 0000000..01b33d9
--- /dev/null
+++ b/src/layout/compoenets/AppMain.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/compoenets/Breadcrumb.vue b/src/layout/compoenets/Breadcrumb.vue
new file mode 100644
index 0000000..48091e8
--- /dev/null
+++ b/src/layout/compoenets/Breadcrumb.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ {{ currentRoute.meta["title"] }}
+
+
+
+
+
+
diff --git a/src/layout/compoenets/LeftMenu.vue b/src/layout/compoenets/LeftMenu.vue
new file mode 100644
index 0000000..917bd25
--- /dev/null
+++ b/src/layout/compoenets/LeftMenu.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+

+
+
+
+
diff --git a/src/layout/index.vue b/src/layout/index.vue
new file mode 100644
index 0000000..957aa21
--- /dev/null
+++ b/src/layout/index.vue
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..a2afdf3
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,13 @@
+import { createApp } from "vue";
+import App from "./App.vue";
+import router from "./router";
+import "./styles/index.scss";
+import ElementPlus from "element-plus";
+
+createApp(App)
+ .use(router)
+ .use(ElementPlus)
+ .mount("#app")
+ .$nextTick(() => {
+ postMessage({ payload: "removeLoading" }, "*");
+ });
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..af55e5b
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,82 @@
+import { createRouter, createWebHistory, createWebHashHistory, RouteRecordRaw } from "vue-router";
+
+const Layout = () => import("@/layout/index.vue");
+
+const routes: RouteRecordRaw[] = [
+ {
+ path: "/",
+ name: "Index",
+ component: Layout,
+ redirect: "/home",
+ children: [
+ {
+ path: "/home",
+ name: "Home",
+ meta: {
+ title: "连接",
+ icon: "material-symbols:rocket-launch-rounded",
+ keepAlive: true
+ },
+ component: () => import("@/views/home/index.vue")
+ },
+ {
+ path: "/proxy",
+ name: "Proxy",
+ meta: {
+ title: "穿透列表",
+ icon: "material-symbols:cloud",
+ keepAlive: true
+ },
+ component: () => import("@/views/proxy/index.vue")
+ },
+ {
+ path: "/download",
+ name: "Download",
+ meta: {
+ title: "版本下载",
+ icon: "material-symbols:download-2",
+ keepAlive: true
+ },
+ component: () => import("@/views/download/index.vue")
+ },
+ {
+ path: "/config",
+ name: "Config",
+ meta: {
+ title: "系统配置",
+ icon: "material-symbols:settings",
+ keepAlive: true
+ },
+ component: () => import("@/views/config/index.vue")
+ },
+ {
+ path: "/logger",
+ name: "Logger",
+ meta: {
+ title: "日志",
+ icon: "material-symbols:file-copy-sharp",
+ keepAlive: true,
+ hidden: false
+ },
+ component: () => import("@/views/logger/index.vue")
+ }
+ // {
+ // path: "/comingSoon",
+ // name: "ComingSoon",
+ // meta: {
+ // title: "敬请期待",
+ // icon: "material-symbols:file-copy-sharp",
+ // keepAlive: false,
+ // hidden: true
+ // },
+ // component: () => import("@/views/comingSoon/index.vue")
+ // }
+ ]
+ }
+];
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes
+});
+export default router;
diff --git a/src/styles/element.scss b/src/styles/element.scss
new file mode 100644
index 0000000..c3f9dad
--- /dev/null
+++ b/src/styles/element.scss
@@ -0,0 +1,9 @@
+@forward 'element-plus/theme-chalk/src/common/var.scss' with (
+ $colors: (
+ 'primary': (
+ 'base': #5F3BB0,
+ ),
+ ),
+);
+
+@use "element-plus/theme-chalk/src/index.scss" as *;
diff --git a/src/styles/index.scss b/src/styles/index.scss
new file mode 100644
index 0000000..892b598
--- /dev/null
+++ b/src/styles/index.scss
@@ -0,0 +1,21 @@
+@import "reset";
+@import "layout";
+@import "element";
+@import "tailwind";
+/* 自定义全局 CssVar */
+:root {
+ --pure-transition-duration: 0.016s;
+}
+
+/* 灰色模式 */
+.html-grey {
+ filter: grayscale(100%);
+}
+
+/* 色弱模式 */
+.html-weakness {
+ filter: invert(80%);
+}
+
+html {
+}
diff --git a/src/styles/layout.scss b/src/styles/layout.scss
new file mode 100644
index 0000000..8970772
--- /dev/null
+++ b/src/styles/layout.scss
@@ -0,0 +1,86 @@
+$main-bg: #F3F3F3;
+$primary-color: #5F3BB0;
+
+.main-container {
+ background: $main-bg;
+ width: calc(100% - 60px);
+ height: 100vh;
+ padding: 20px;
+
+ .main {
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ .app-container-breadcrumb {
+ height: calc(100% - 50px);
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+
+ .breadcrumb {
+ color: $primary-color;
+ font-size: 36px;
+ height: 50px;
+
+ svg {
+ vertical-align: top;
+ }
+
+ span {
+ vertical-align: top;
+ color: black;
+ font-size: 18px;
+ font-weight: bold;
+ transform: translateY(5px);
+ display: inline-block;
+
+ }
+ }
+}
+
+.left-menu-container {
+ background: #fff;
+ width: 60px;
+ height: 100vh;
+
+ .menu-container {
+
+ .menu {
+ display: flex;
+ height: 40px;
+ justify-content: center;
+ align-items: center;
+ font-size: 20px;
+ color: #ADADAD;
+ margin-bottom: 10px;
+ }
+
+ .menu-selected {
+ background: #EBE6F7;
+ color: $primary-color;
+ }
+
+ .menu:hover {
+ color: $primary-color;
+ cursor: pointer;
+ }
+ }
+}
+
+.logo-container {
+ height: 60px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .logo {
+ width: 60%;
+ }
+}
+
+
+.primary-text {
+ color: $primary-color;
+}
diff --git a/src/styles/reset.scss b/src/styles/reset.scss
new file mode 100644
index 0000000..9d1d252
--- /dev/null
+++ b/src/styles/reset.scss
@@ -0,0 +1,275 @@
+*,
+::before,
+::after {
+ box-sizing: border-box;
+ border-width: 0;
+ border-style: solid;
+ border-color: currentColor;
+}
+
+*{
+ -webkit-touch-callout:none; /*系统默认菜单被禁用*/
+ -webkit-user-select:none; /*webkit浏览器*/
+ -khtml-user-select:none; /*早期浏览器*/
+ -moz-user-select:none;/*火狐*/
+ -ms-user-select:none; /*IE10*/
+ user-select:none;
+}
+
+input{
+ -webkit-user-select:auto; /*webkit浏览器*/
+}
+textarea{
+ -webkit-user-select:auto; /*webkit浏览器*/
+}
+
+#app {
+ width: 100%;
+ height: 100%;
+}
+
+html {
+ line-height: 1.5;
+ -webkit-text-size-adjust: 100%;
+ -moz-tab-size: 4;
+ tab-size: 4;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ line-height: inherit;
+ width: 100%;
+ height: 100%;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizelegibility;
+ font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
+ "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
+}
+
+hr {
+ height: 0;
+ color: inherit;
+ border-top-width: 1px;
+}
+
+abbr:where([title]) {
+ text-decoration: underline dotted;
+}
+
+a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+code,
+kbd,
+samp,
+pre {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ "Liberation Mono", "Courier New", monospace;
+ font-size: 1em;
+}
+
+small {
+ font-size: 80%;
+}
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+table {
+ text-indent: 0;
+ border-color: inherit;
+ border-collapse: collapse;
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit;
+ font-size: 100%;
+ line-height: inherit;
+ color: inherit;
+ margin: 0;
+ padding: 0;
+}
+
+button,
+select {
+ text-transform: none;
+}
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ background-image: none;
+}
+
+:-moz-focusring {
+ outline: auto;
+}
+
+:-moz-ui-invalid {
+ box-shadow: none;
+}
+
+progress {
+ vertical-align: baseline;
+}
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+ height: auto;
+}
+
+[type="search"] {
+ outline-offset: -2px;
+}
+
+::-webkit-file-upload-button {
+ font: inherit;
+}
+
+summary {
+ display: list-item;
+}
+
+blockquote,
+dl,
+dd,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+figure,
+p,
+pre {
+ margin: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+}
+
+legend {
+ padding: 0;
+}
+
+ol,
+ul,
+menu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+textarea {
+ resize: vertical;
+}
+
+input::placeholder,
+textarea::placeholder {
+ opacity: 1;
+ color: #9ca3af;
+}
+
+button,
+[role="button"] {
+ cursor: pointer;
+}
+
+:disabled {
+ cursor: default;
+}
+
+//img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+ display: block;
+}
+
+img,
+video {
+ max-width: 100%;
+ height: auto;
+}
+
+[hidden] {
+ display: none;
+}
+
+.dark {
+ color-scheme: dark;
+}
+
+label {
+ font-weight: 700;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+
+a:focus,
+a:active {
+ outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+ cursor: pointer;
+ color: inherit;
+ text-decoration: none;
+}
+
+div:focus {
+ outline: none;
+}
+
+.clearfix {
+ &::after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+}
diff --git a/src/styles/tailwind.scss b/src/styles/tailwind.scss
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/src/styles/tailwind.scss
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
new file mode 100644
index 0000000..277926a
--- /dev/null
+++ b/src/types/global.d.ts
@@ -0,0 +1,9 @@
+declare module 'element-plus/dist/locale/zh-cn.mjs' {
+ const zhLocale: any;
+ export default zhLocale;
+}
+
+declare module 'element-plus/dist/locale/en.mjs' {
+ const enLocale: any;
+ export default enLocale;
+}
diff --git a/src/types/shims-tsx.d.ts b/src/types/shims-tsx.d.ts
new file mode 100644
index 0000000..199f979
--- /dev/null
+++ b/src/types/shims-tsx.d.ts
@@ -0,0 +1,22 @@
+import Vue, { VNode } from "vue";
+
+declare module "*.tsx" {
+ import Vue from "compatible-vue";
+ export default Vue;
+}
+
+declare global {
+ namespace JSX {
+ interface Element extends VNode {}
+ interface ElementClass extends Vue {}
+ interface ElementAttributesProperty {
+ $props: any;
+ }
+ interface IntrinsicElements {
+ [elem: string]: any;
+ }
+ interface IntrinsicAttributes {
+ [elem: string]: any;
+ }
+ }
+}
diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts
new file mode 100644
index 0000000..9fa8db3
--- /dev/null
+++ b/src/types/shims-vue.d.ts
@@ -0,0 +1,10 @@
+declare module "*.vue" {
+ import { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
+
+declare module "*.scss" {
+ const scss: Record;
+ export default scss;
+}
diff --git a/src/utils/clone.ts b/src/utils/clone.ts
new file mode 100644
index 0000000..f40201b
--- /dev/null
+++ b/src/utils/clone.ts
@@ -0,0 +1,19 @@
+export function clone(value: T): T {
+ /** 空 */
+ if (!value) return value;
+ /** 数组 */
+ if (Array.isArray(value))
+ return value.map(item => clone(item)) as unknown as T;
+ /** 日期 */
+ if (value instanceof Date) return new Date(value) as unknown as T;
+ /** 普通对象 */
+ if (typeof value === "object") {
+ return Object.fromEntries(
+ Object.entries(value).map(([k, v]: [string, any]) => {
+ return [k, clone(v)];
+ })
+ ) as unknown as T;
+ }
+ /** 基本类型 */
+ return value;
+}
diff --git a/src/views/comingSoon/index.vue b/src/views/comingSoon/index.vue
new file mode 100644
index 0000000..229688d
--- /dev/null
+++ b/src/views/comingSoon/index.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/src/views/config/index.vue b/src/views/config/index.vue
new file mode 100644
index 0000000..8f4284c
--- /dev/null
+++ b/src/views/config/index.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 手动刷新
+
+
+
+ 点击这里去下载
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保 存
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/download/index.vue b/src/views/download/index.vue
new file mode 100644
index 0000000..75042a4
--- /dev/null
+++ b/src/views/download/index.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+ {{ version.name }}
+
+
+ 发布时间:{{
+ moment(version.published_at).format("YYYY-MM-DD HH:mm:ss")
+ }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/home/index.vue b/src/views/home/index.vue
new file mode 100644
index 0000000..7a5526e
--- /dev/null
+++ b/src/views/home/index.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Frpc {{ running ? "已启动" : "已断开" }}
+
+
+
查看日志
+
+
+ {{ running ? "断 开" : "启 动" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/logger/index.vue b/src/views/logger/index.vue
new file mode 100644
index 0000000..a24402e
--- /dev/null
+++ b/src/views/logger/index.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
diff --git a/src/views/proxy/index.vue b/src/views/proxy/index.vue
new file mode 100644
index 0000000..02a1e99
--- /dev/null
+++ b/src/views/proxy/index.vue
@@ -0,0 +1,487 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ proxy.type }}
+
+
+
{{ proxy.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修 改
+
+
+
+ 删 除
+
+
+
+
+
+
+
+
+
内网地址
+
{{ proxy.localIp }}
+
+
+
外网端口
+
{{ proxy.remotePort }}
+
+
+
内网端口
+
{{ proxy.localPort }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关 闭
+ 保 存
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..11bb37f
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,20 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: "class",
+ corePlugins: {
+ preflight: false
+ },
+ content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ bg_color: "var(--el-bg-color)",
+ primary: "var(--el-color-primary)",
+ primary_light_9: "var(--el-color-primary-light-9)",
+ text_color_primary: "var(--el-text-color-primary)",
+ text_color_regular: "var(--el-text-color-regular)",
+ text_color_disabled: "var(--el-text-color-disabled)"
+ }
+ }
+ }
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..afca1ed
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,49 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "strict": false,
+ "jsx": "preserve",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "lib": [
+ "ESNext",
+ "DOM"
+ ],
+ "skipLibCheck": true,
+ "noEmit": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "src/*"
+ ]
+ },
+ "types": [
+ "node",
+ "vite/client",
+ "element-plus/global"
+ ],
+ "typeRoots": [
+ "./types",
+ "./node_modules/@types/",
+ "./node_modules"
+ ]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "src/**/*.vue",
+ "types/*.d.ts",
+ "package.json",
+ "electron",
+ "vite.config.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/*.js"
+ ]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..c8b26b9
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,105 @@
+import { rmSync } from "node:fs";
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import electron from "vite-plugin-electron";
+import renderer from "vite-plugin-electron-renderer";
+import { notBundle } from "vite-plugin-electron/plugin";
+import { resolve } from "path";
+
+
+import pkg from "./package.json";
+
+/** 路径查找 */
+const pathResolve = (dir: string): string => {
+ return resolve(__dirname, ".", dir);
+};
+
+// https://vitejs.dev/config/
+export default defineConfig(({ command }) => {
+ rmSync("dist-electron", { recursive: true, force: true });
+
+ const isServe = command === "serve";
+ const isBuild = command === "build";
+ const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
+
+ return {
+ plugins: [
+ vue(),
+ electron([
+ {
+ // Main process entry file of the Electron App.
+ entry: "electron/main/index.ts",
+ onstart({ startup }) {
+ if (process.env.VSCODE_DEBUG) {
+ console.log(
+ /* For `.vscode/.debug.script.mjs` */ "[startup] Electron App"
+ );
+ } else {
+ startup();
+ }
+ },
+ vite: {
+ build: {
+ sourcemap,
+ minify: isBuild,
+ outDir: "dist-electron/main",
+ rollupOptions: {
+ // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons,
+ // we can use `external` to exclude them to ensure they work correctly.
+ // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built.
+ // Of course, this is not absolute, just this way is relatively simple. :)
+ external: Object.keys(
+ "dependencies" in pkg ? pkg.dependencies : {}
+ )
+ }
+ },
+ plugins: [
+ // This is just an option to improve build performance, it's non-deterministic!
+ // e.g. `import log from 'electron-log'` -> `const log = require('electron-log')`
+ isServe && notBundle()
+ ]
+ }
+ },
+ {
+ entry: "electron/preload/index.ts",
+ onstart({ reload }) {
+ // Notify the Renderer process to reload the page when the Preload scripts build is complete,
+ // instead of restarting the entire Electron App.
+ reload();
+ },
+ vite: {
+ build: {
+ sourcemap: sourcemap ? "inline" : undefined, // #332
+ minify: isBuild,
+ outDir: "dist-electron/preload",
+ rollupOptions: {
+ external: Object.keys(
+ "dependencies" in pkg ? pkg.dependencies : {}
+ )
+ }
+ },
+ plugins: [isServe && notBundle()]
+ }
+ }
+ ]),
+ // Use Node.js API in the Renderer process
+ renderer()
+ ],
+ resolve: {
+ alias: {
+ "@": pathResolve("src"),
+ "@build": pathResolve("build")
+ }
+ },
+ server:
+ process.env.VSCODE_DEBUG &&
+ (() => {
+ const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL);
+ return {
+ host: url.hostname,
+ port: +url.port
+ };
+ })(),
+ clearScreen: false
+ };
+});