🎉 首次提交
30
.gitignore
vendored
Normal file
@ -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
|
6
.prettierrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
bracketSpacing: true,
|
||||
singleQuote: false,
|
||||
arrowParens: "avoid",
|
||||
trailingComma: "none"
|
||||
};
|
2
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
|
||||
|
46
electron-builder.json5
Normal file
@ -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}"
|
||||
},
|
||||
}
|
42
electron/api/config.ts
Normal file
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
183
electron/api/frpc.ts
Normal file
@ -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();
|
||||
}
|
||||
});
|
||||
};
|
121
electron/api/github.ts
Normal file
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
30
electron/api/logger.ts
Normal file
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
69
electron/api/proxy.ts
Normal file
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
11
electron/electron-env.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/// <reference types="vite-plugin-electron/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
VSCODE_DEBUG?: 'true'
|
||||
DIST_ELECTRON: string
|
||||
DIST: string
|
||||
/** /dist/ or /public/ */
|
||||
VITE_PUBLIC: string
|
||||
}
|
||||
}
|
136
electron/main/index.ts
Normal file
@ -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();
|
127
electron/main/index111.ts
Normal file
@ -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();
|
92
electron/preload/index.ts
Normal file
@ -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 = `<div class="${className}"><div></div></div>`
|
||||
|
||||
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)
|
37
electron/storage/config.ts
Normal file
@ -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);
|
||||
};
|
74
electron/storage/proxy.ts
Normal file
@ -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);
|
||||
};
|
38
electron/storage/version.ts
Normal file
@ -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);
|
||||
};
|
14
index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo/32x32.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||
<title>Frpc Desktop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
62
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
8
postcss.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
|
||||
}
|
||||
};
|
BIN
public/logo/1024x1024.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
public/logo/128x128.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/logo/16x16.png
Normal file
After Width: | Height: | Size: 352 B |
BIN
public/logo/24x24.png
Normal file
After Width: | Height: | Size: 488 B |
BIN
public/logo/256x256.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
public/logo/32x32.png
Normal file
After Width: | Height: | Size: 600 B |
BIN
public/logo/48x48.png
Normal file
After Width: | Height: | Size: 819 B |
BIN
public/logo/512x512.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
public/logo/64x64.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
23
src/App.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { ElConfigProvider } from "element-plus";
|
||||
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
|
||||
|
||||
export default defineComponent({
|
||||
name: "app",
|
||||
components: {
|
||||
[ElConfigProvider.name]: ElConfigProvider
|
||||
},
|
||||
computed: {
|
||||
currentLocale() {
|
||||
return zhCn;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="currentLocale">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
BIN
src/assets/logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
22
src/layout/compoenets/AppMain.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent } from "vue";
|
||||
import router from "@/router";
|
||||
|
||||
defineComponent({
|
||||
name: "AppMain"
|
||||
});
|
||||
|
||||
const currentRoute = computed(() => {
|
||||
return router.currentRoute.value;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive v-if="currentRoute.meta['keepAlive']">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
<component v-else :is="Component" />
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
28
src/layout/compoenets/Breadcrumb.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { computed, defineComponent } from "vue";
|
||||
import router from "@/router";
|
||||
|
||||
defineComponent({
|
||||
name: "Breadcrumb"
|
||||
});
|
||||
|
||||
const currentRoute = computed(() => {
|
||||
return router.currentRoute.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-between">
|
||||
<div class="breadcrumb">
|
||||
<Icon
|
||||
class="inline-block mr-2"
|
||||
:icon="currentRoute.meta['icon'] as string"
|
||||
/>
|
||||
<span>{{ currentRoute.meta["title"] }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
54
src/layout/compoenets/LeftMenu.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, onMounted, ref } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import router from "@/router";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
|
||||
defineComponent({
|
||||
name: "AppMain"
|
||||
});
|
||||
|
||||
const routes = ref<Array<RouteRecordRaw>>([]);
|
||||
|
||||
const currentRoute = computed(() => {
|
||||
return router.currentRoute.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 菜单切换
|
||||
* @param mi 菜单索引
|
||||
*/
|
||||
const handleMenuChange = (route: RouteRecordRaw) => {
|
||||
if (currentRoute.value.name === route.name) {
|
||||
return;
|
||||
}
|
||||
router.push({
|
||||
path: route.path
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
routes.value = router.options.routes[0].children?.filter(
|
||||
f => !f.meta?.hidden
|
||||
) as Array<RouteRecordRaw>;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="left-menu-container drop-shadow-xl">
|
||||
<div class="logo-container">
|
||||
<img src="/logo/64x64.png" class="logo" alt="Logo" />
|
||||
</div>
|
||||
<ul class="menu-container">
|
||||
<li
|
||||
class="menu"
|
||||
:class="currentRoute?.name === r.name ? 'menu-selected' : ''"
|
||||
v-for="r in routes"
|
||||
:key="r.name"
|
||||
@click="handleMenuChange(r)"
|
||||
>
|
||||
<Icon :icon="r?.meta?.icon as string" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
15
src/layout/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from "vue";
|
||||
import AppMain from "./compoenets/AppMain.vue";
|
||||
import LeftMenu from "./compoenets/LeftMenu.vue";
|
||||
|
||||
defineComponent({
|
||||
name: "Layout"
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="w-full h-full flex">
|
||||
<left-menu />
|
||||
<app-main />
|
||||
</div>
|
||||
</template>
|
13
src/main.ts
Normal file
@ -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" }, "*");
|
||||
});
|
82
src/router/index.ts
Normal file
@ -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;
|
9
src/styles/element.scss
Normal file
@ -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 *;
|
21
src/styles/index.scss
Normal file
@ -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 {
|
||||
}
|
86
src/styles/layout.scss
Normal file
@ -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;
|
||||
}
|
275
src/styles/reset.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
3
src/styles/tailwind.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
9
src/types/global.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
22
src/types/shims-tsx.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
10
src/types/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module "*.scss" {
|
||||
const scss: Record<string, string>;
|
||||
export default scss;
|
||||
}
|
19
src/utils/clone.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export function clone<T>(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;
|
||||
}
|
15
src/views/comingSoon/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
defineComponent({
|
||||
name: "ComingSoon"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-full bg-white rounded p-2 overflow-hidden drop-shadow-xl flex justify-center items-center"
|
||||
>
|
||||
<el-empty description="敬请期待" />
|
||||
</div>
|
||||
</template>
|
203
src/views/config/index.vue
Normal file
@ -0,0 +1,203 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, onUnmounted, ref, reactive } from "vue";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { ElMessage, FormInstance, FormRules } from "element-plus";
|
||||
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { clone } from "@/utils/clone";
|
||||
import { Icon } from "@iconify/vue";
|
||||
|
||||
defineComponent({
|
||||
name: "Config"
|
||||
});
|
||||
|
||||
type Config = {
|
||||
currentVersion: string;
|
||||
serverAddr: string;
|
||||
serverPort: number;
|
||||
authMethod: string;
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
type Version = {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const formData = ref<Config>({
|
||||
currentVersion: "",
|
||||
serverAddr: "",
|
||||
serverPort: 7000,
|
||||
authMethod: "",
|
||||
authToken: ""
|
||||
});
|
||||
|
||||
const loading = ref(1);
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
currentVersion: [{ required: true, message: "请选择版本", trigger: "blur" }],
|
||||
serverAddr: [
|
||||
{ required: true, message: "请输入服务端地址", trigger: "blur" },
|
||||
{
|
||||
pattern: /^[\w-]+(\.[\w-]+)+$/,
|
||||
message: "请输入正确的服务端地址",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
serverPort: [
|
||||
{ required: true, message: "请输入服务器端口", trigger: "blur" }
|
||||
],
|
||||
// authMethod: [{ required: true, message: "请选择验证方式", trigger: "blur" }],
|
||||
authToken: [{ required: true, message: "请输入token值 ", trigger: "blur" }]
|
||||
});
|
||||
|
||||
const versions = ref<Array<Version>>([]);
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const handleSubmit = useDebounceFn(() => {
|
||||
if (!formRef.value) return;
|
||||
formRef.value.validate(valid => {
|
||||
if (valid) {
|
||||
loading.value = 1;
|
||||
const data = clone(formData.value);
|
||||
ipcRenderer.send("config.saveConfig", data);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
|
||||
const handleLoadVersions = () => {
|
||||
ipcRenderer.send("config.versions");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
ipcRenderer.send("config.getConfig");
|
||||
handleLoadVersions();
|
||||
ipcRenderer.on("Config.getConfig.hook", (event, args) => {
|
||||
const { err, data } = args;
|
||||
if (!err) {
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
}
|
||||
}
|
||||
loading.value--;
|
||||
});
|
||||
|
||||
ipcRenderer.on("Config.saveConfig.hook", (event, args) => {
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "保存成功"
|
||||
});
|
||||
loading.value--;
|
||||
});
|
||||
ipcRenderer.on("Config.versions.hook", (event, args) => {
|
||||
const { err, data } = args;
|
||||
if (!err) {
|
||||
versions.value = data;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ipcRenderer.removeAllListeners("Config.getConfig.hook");
|
||||
ipcRenderer.removeAllListeners("Config.saveConfig.hook");
|
||||
ipcRenderer.removeAllListeners("Config.versions.hook");
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<breadcrumb />
|
||||
<div
|
||||
class="w-full bg-white p-4 rounded drop-shadow-lg"
|
||||
v-loading="loading > 0"
|
||||
>
|
||||
<el-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
ref="formRef"
|
||||
label-width="120"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="选择版本:" prop="currentVersion">
|
||||
<el-select
|
||||
v-model="formData.currentVersion"
|
||||
class="w-full"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="v in versions"
|
||||
:key="v.id"
|
||||
:label="v.name"
|
||||
:value="v.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<div class="w-full flex justify-end">
|
||||
<el-button type="text" @click="handleLoadVersions">
|
||||
<Icon class="mr-1" icon="material-symbols:refresh-rounded" />
|
||||
手动刷新
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="$router.replace({ name: 'Download' })"
|
||||
>
|
||||
<Icon class="mr-1" icon="material-symbols:download-2" />
|
||||
点击这里去下载
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="服务端地址:" prop="serverAddr">
|
||||
<el-input
|
||||
v-model="formData.serverAddr"
|
||||
placeholder="127.0.0.1"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="服务端端口:" prop="serverPort">
|
||||
<el-input-number
|
||||
placeholder="7000"
|
||||
v-model="formData.serverPort"
|
||||
:min="0"
|
||||
:max="65535"
|
||||
controls-position="right"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="验证方式:" prop="authMethod">
|
||||
<el-select
|
||||
v-model="formData.authMethod"
|
||||
placeholder="请选择验证方式"
|
||||
clearable
|
||||
>
|
||||
<el-option label="token" value="token"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" v-if="formData.authMethod === 'token'">
|
||||
<el-form-item label="token:" prop="authToken">
|
||||
<el-input
|
||||
placeholder="token"
|
||||
type="password"
|
||||
v-model="formData.authToken"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item>
|
||||
<el-button plain type="primary" @click="handleSubmit">
|
||||
<Icon class="mr-1" icon="material-symbols:save" />
|
||||
保 存
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
116
src/views/download/index.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
||||
import { ipcRenderer } from "electron";
|
||||
import moment from "moment";
|
||||
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
|
||||
defineComponent({
|
||||
name: "Download"
|
||||
});
|
||||
|
||||
type Version = {
|
||||
id: string;
|
||||
name: string;
|
||||
published_at: string;
|
||||
download_completed: boolean;
|
||||
};
|
||||
|
||||
const versions = ref<Array<Version>>([]);
|
||||
const loading = ref(1);
|
||||
const downloadPercentage = ref(0);
|
||||
const downloading = ref<Map<string, number>>(new Map<string, number>());
|
||||
|
||||
/**
|
||||
* 获取版本
|
||||
*/
|
||||
const handleLoadVersions = () => {
|
||||
ipcRenderer.send("github.getFrpVersions");
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载
|
||||
* @param version
|
||||
*/
|
||||
const handleDownload = (version: Version) => {
|
||||
ipcRenderer.send("github.download", version.id);
|
||||
downloading.value.set(version.id, 0);
|
||||
};
|
||||
|
||||
const handleInitDownloadHook = () => {
|
||||
ipcRenderer.on("Download.frpVersionHook", (event, args) => {
|
||||
loading.value--;
|
||||
versions.value = args as Array<Version>;
|
||||
});
|
||||
// 进度监听
|
||||
ipcRenderer.on("Download.frpVersionDownloadOnProgress", (event, args) => {
|
||||
const { id, progress } = args;
|
||||
downloading.value.set(
|
||||
id,
|
||||
Number(Number(progress.percent * 100).toFixed(2))
|
||||
);
|
||||
});
|
||||
ipcRenderer.on("Download.frpVersionDownloadOnCompleted", (event, args) => {
|
||||
downloading.value.delete(args);
|
||||
const version: Version | undefined = versions.value.find(
|
||||
f => f.id === args
|
||||
);
|
||||
if (version) {
|
||||
version.download_completed = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleLoadVersions();
|
||||
handleInitDownloadHook();
|
||||
// ipcRenderer.invoke("process").then((r: any) => {
|
||||
// console.log(r, "rrr");
|
||||
// });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnProgress");
|
||||
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnCompleted");
|
||||
ipcRenderer.removeAllListeners("Download.frpVersionHook");
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="main">
|
||||
<breadcrumb />
|
||||
<div class="app-container-breadcrumb" v-loading="loading > 0">
|
||||
<div
|
||||
class="w-full bg-white mb-4 rounded p-4 drop-shadow-lg flex justify-between items-center"
|
||||
v-for="version in versions"
|
||||
:key="version.id"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="mb-2">
|
||||
<el-tag>{{ version.name }}</el-tag>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
发布时间:<span class="text-gray-00">{{
|
||||
moment(version.published_at).format("YYYY-MM-DD HH:mm:ss")
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span
|
||||
class="primary-text text-sm font-bold"
|
||||
v-if="version.download_completed"
|
||||
>已下载</span
|
||||
>
|
||||
<template v-else>
|
||||
<div class="w-32" v-if="downloading.has(version.id)">
|
||||
<el-progress
|
||||
:percentage="downloading.get(version.id)"
|
||||
:text-inside="false"
|
||||
/>
|
||||
</div>
|
||||
<el-button v-else @click="handleDownload(version)">下载</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
199
src/views/home/index.vue
Normal file
@ -0,0 +1,199 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
||||
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import router from "@/router";
|
||||
|
||||
defineComponent({
|
||||
name: "Home"
|
||||
});
|
||||
|
||||
const running = ref(false);
|
||||
|
||||
const handleStartFrpc = () => {
|
||||
ipcRenderer.send("frpc.start");
|
||||
};
|
||||
|
||||
const handleStopFrpc = () => {
|
||||
ipcRenderer.send("frpc.stop");
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (running.value) {
|
||||
handleStopFrpc();
|
||||
} else {
|
||||
handleStartFrpc();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setInterval(() => {
|
||||
ipcRenderer.invoke("frpc.running").then(data => {
|
||||
running.value = data;
|
||||
});
|
||||
}, 300);
|
||||
|
||||
ipcRenderer.on("Home.frpc.start.error.hook", (event, args) => {
|
||||
if (args) {
|
||||
ElMessageBox.alert(args, "启动失败", {
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: "去设置"
|
||||
}).then(() => {
|
||||
router.replace({
|
||||
name: "Config"
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ipcRenderer.removeAllListeners("Home.frpc.start.error.hook");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main">
|
||||
<breadcrumb />
|
||||
<div class="app-container-breadcrumb">
|
||||
<div
|
||||
class="w-full h-full bg-white p-4 rounded drop-shadow-lg overflow-y-auto flex justify-center items-center"
|
||||
>
|
||||
<div class="flex">
|
||||
<div
|
||||
class="w-40 h-40 border-[#5A3DAA] text-[#5A3DAA] border-4 rounded-full flex justify-center items-center text-[100px] relative"
|
||||
>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="running"
|
||||
class="z-0 rounded-full opacity-20 left-circle bg-[#5A3DAA] w-full h-full animation-rotate-1"
|
||||
/>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="running"
|
||||
class="z-0 rounded-full opacity-20 right-circle top-[10px] bg-[#5A3DAA] w-full h-full animation-rotate-2"
|
||||
/>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="running"
|
||||
class="z-0 rounded-full opacity-20 top-circle bg-[#5A3DAA] w-full h-full animation-rotate-3"
|
||||
/>
|
||||
</transition>
|
||||
<div
|
||||
class="bg-white z-10 w-full h-full bg-white absolute rounded-full flex justify-center items-center"
|
||||
>
|
||||
<Icon icon="material-symbols:rocket-launch-rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="pl-8 h-28 w-52 flex flex-col justify-between">
|
||||
<transition name="fade">
|
||||
<div class="font-bold text-2xl text-center">
|
||||
Frpc {{ running ? "已启动" : "已断开" }}
|
||||
</div>
|
||||
</transition>
|
||||
<el-button
|
||||
class="block"
|
||||
type="text"
|
||||
v-if="running"
|
||||
@click="$router.replace({ name: 'Logger' })"
|
||||
>查看日志
|
||||
</el-button>
|
||||
<div
|
||||
class="w-full h-8 bg-[#563EA4] rounded flex justify-center items-center text-white font-bold cursor-pointer"
|
||||
@click="handleButtonClick"
|
||||
>
|
||||
{{ running ? "断 开" : "启 动" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <el-button-->
|
||||
<!-- plain-->
|
||||
<!-- type="primary"-->
|
||||
<!-- @click="handleStartFrpc"-->
|
||||
<!-- :disabled="running"-->
|
||||
<!-- >启动-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button-->
|
||||
<!-- plain-->
|
||||
<!-- type="danger"-->
|
||||
<!-- :disabled="!running"-->
|
||||
<!-- @click="handleStopFrpc"-->
|
||||
<!-- >停止-->
|
||||
<!-- </el-button>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes transform-opacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
$offset: 10px;
|
||||
|
||||
.animation-rotate-1 {
|
||||
animation: rotate 5s linear infinite;
|
||||
}
|
||||
|
||||
.animation-rotate-2 {
|
||||
animation: rotate 4s linear infinite;
|
||||
}
|
||||
|
||||
.animation-rotate-3 {
|
||||
animation: rotate 6s linear infinite;
|
||||
}
|
||||
|
||||
.top-circle {
|
||||
position: absolute;
|
||||
bottom: $offset;
|
||||
transform-origin: center calc(50% - $offset);
|
||||
}
|
||||
|
||||
.left-circle {
|
||||
position: absolute;
|
||||
left: $offset;
|
||||
top: $offset;
|
||||
transform-origin: calc(50% + $offset) center;
|
||||
//transform-origin: calc(50% - 5px) center;
|
||||
}
|
||||
|
||||
.right-circle {
|
||||
position: absolute;
|
||||
right: $offset;
|
||||
top: $offset;
|
||||
transform-origin: calc(50% - $offset) center;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
67
src/views/logger/index.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
||||
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
defineComponent({
|
||||
name: "Logger"
|
||||
});
|
||||
|
||||
const loggerContent = ref('<div class="text-white">暂无日志</div>');
|
||||
|
||||
const handleLog2Html = (logContent: string) => {
|
||||
const logs = logContent
|
||||
.split("\n")
|
||||
.filter(f => f)
|
||||
.map(m => {
|
||||
if (m.indexOf("[E]") !== -1) {
|
||||
return `<div class="text-[#FF0006]">${m}</div> `;
|
||||
} else if (m.indexOf("[I]") !== -1) {
|
||||
return `<div class="text-[#48BB31]">${m}</div> `;
|
||||
} else if (m.indexOf("[D]") !== -1) {
|
||||
return `<div class="text-[#0070BB]">${m}</div> `;
|
||||
} else if (m.indexOf("[W]") !== -1) {
|
||||
return `<div class="text-[#BBBB23]">${m}</div> `;
|
||||
} else {
|
||||
return `<div class="text-[#BBBBBB]">${m}</div> `;
|
||||
}
|
||||
});
|
||||
return logs.reverse().join("");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
ipcRenderer.send("logger.getLog");
|
||||
ipcRenderer.on("Logger.getLog.hook", (event, args) => {
|
||||
// console.log("日志", args, args.indexOf("\n"));
|
||||
// const logs = args.split("\n");
|
||||
// console.log(logs, "2");
|
||||
if (args) {
|
||||
loggerContent.value = handleLog2Html(args);
|
||||
}
|
||||
ipcRenderer.send("logger.update");
|
||||
});
|
||||
ipcRenderer.on("Logger.update.hook", (event, args) => {
|
||||
if (args) {
|
||||
loggerContent.value = handleLog2Html(args);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ipcRenderer.removeAllListeners("Logger.getLog.hook");
|
||||
ipcRenderer.removeAllListeners("Logger.update.hook");
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="main">
|
||||
<breadcrumb />
|
||||
<div class="app-container-breadcrumb">
|
||||
<div
|
||||
class="w-full h-full p-4 bg-[#2B2B2B] rounded drop-shadow-lg overflow-y-auto"
|
||||
v-html="loggerContent"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
487
src/views/proxy/index.vue
Normal file
@ -0,0 +1,487 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
|
||||
import { ElMessage, FormInstance, FormRules } from "element-plus";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { clone } from "@/utils/clone";
|
||||
|
||||
defineComponent({
|
||||
name: "Proxy"
|
||||
});
|
||||
|
||||
type Proxy = {
|
||||
_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
localIp: string;
|
||||
localPort: number;
|
||||
remotePort: number;
|
||||
customDomains: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 代理列表
|
||||
*/
|
||||
const proxys = ref<Array<Proxy>>([]);
|
||||
/**
|
||||
* loading
|
||||
*/
|
||||
const loading = ref({
|
||||
list: 1,
|
||||
form: 0
|
||||
});
|
||||
|
||||
/**
|
||||
* 弹出层属性
|
||||
*/
|
||||
const edit = ref({
|
||||
title: "新增代理",
|
||||
visible: false
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单内容
|
||||
*/
|
||||
const editForm = ref<Proxy>({
|
||||
_id: "",
|
||||
name: "",
|
||||
type: "http",
|
||||
localIp: "",
|
||||
localPort: 8080,
|
||||
remotePort: 8080,
|
||||
customDomains: [""]
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单校验
|
||||
*/
|
||||
const editFormRules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: "请输入名称", trigger: "blur" },
|
||||
// {
|
||||
// pattern: /^[a-zA-Z]+$/,
|
||||
// message: "名称只能是英文",
|
||||
// trigger: "blur"
|
||||
// }
|
||||
],
|
||||
type: [{ required: true, message: "请选择类型", trigger: "blur" }],
|
||||
localIp: [
|
||||
{ required: true, message: "请输入内网地址", trigger: "blur" },
|
||||
{
|
||||
pattern: /^[\w-]+(\.[\w-]+)+$/,
|
||||
message: "请输入正确的内网地址",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
localPort: [{ required: true, message: "请输入本地端口", trigger: "blur" }],
|
||||
remotePort: [{ required: true, message: "请输入远程端口", trigger: "blur" }]
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单dom
|
||||
*/
|
||||
const editFormRef = ref<FormInstance>();
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!editFormRef.value) return;
|
||||
await editFormRef.value.validate(valid => {
|
||||
if (valid) {
|
||||
loading.value.form = 1;
|
||||
const data = clone(editForm.value);
|
||||
if (data._id) {
|
||||
ipcRenderer.send("proxy.updateProxy", data);
|
||||
} else {
|
||||
ipcRenderer.send("proxy.insertProxy", data);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加代理域名
|
||||
*/
|
||||
const handleAddDomain = () => {
|
||||
editForm.value.customDomains.push("");
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除代理列表
|
||||
* @param index
|
||||
*/
|
||||
const handleDeleteDomain = (index: number) => {
|
||||
editForm.value.customDomains.splice(index, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载代理
|
||||
*/
|
||||
const handleLoadProxys = () => {
|
||||
ipcRenderer.send("proxy.getProxys");
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除代理
|
||||
* @param proxy
|
||||
*/
|
||||
const handleDeleteProxy = (proxy: Proxy) => {
|
||||
ipcRenderer.send("proxy.deleteProxyById", proxy._id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
const handleResetForm = () => {
|
||||
editForm.value = {
|
||||
_id: "",
|
||||
name: "",
|
||||
type: "http",
|
||||
localIp: "",
|
||||
localPort: 0,
|
||||
remotePort: 0,
|
||||
customDomains: [""]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化回调
|
||||
*/
|
||||
const handleInitHook = () => {
|
||||
const InsertOrUpdateHook = (message: string, args: any) => {
|
||||
loading.value.form--;
|
||||
const { err } = args;
|
||||
if (!err) {
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: message
|
||||
});
|
||||
handleResetForm();
|
||||
handleLoadProxys();
|
||||
edit.value.visible = false;
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.on("Proxy.insertProxy.hook", (event, args) => {
|
||||
InsertOrUpdateHook("新增成功", args);
|
||||
});
|
||||
ipcRenderer.on("Proxy.updateProxy.hook", (event, args) => {
|
||||
InsertOrUpdateHook("修改成功", args);
|
||||
});
|
||||
// ipcRenderer.on("Proxy.updateProxy.hook", (event, args) => {
|
||||
// loading.value.form--;
|
||||
// const { err } = args;
|
||||
// if (!err) {
|
||||
// ElMessage({
|
||||
// type: "success",
|
||||
// message: "修改成功"
|
||||
// });
|
||||
// handleResetForm();
|
||||
// handleLoadProxys();
|
||||
// edit.value.visible = false;
|
||||
// }
|
||||
// });
|
||||
ipcRenderer.on("Proxy.getProxys.hook", (event, args) => {
|
||||
loading.value.list--;
|
||||
const { err, data } = args;
|
||||
if (!err) {
|
||||
proxys.value = data;
|
||||
}
|
||||
});
|
||||
ipcRenderer.on("Proxy.deleteProxyById.hook", (event, args) => {
|
||||
const { err, data } = args;
|
||||
if (!err) {
|
||||
handleLoadProxys();
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "删除成功"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleOpenInsert = () => {
|
||||
edit.value = {
|
||||
title: "新增代理",
|
||||
visible: true
|
||||
};
|
||||
};
|
||||
|
||||
const handleOpenUpdate = (proxy: Proxy) => {
|
||||
editForm.value = clone(proxy);
|
||||
edit.value = {
|
||||
title: "修改代理",
|
||||
visible: true
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleInitHook();
|
||||
handleLoadProxys();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ipcRenderer.removeAllListeners("Proxy.insertProxy.hook");
|
||||
ipcRenderer.removeAllListeners("Proxy.updateProxy.hook");
|
||||
ipcRenderer.removeAllListeners("Proxy.deleteProxyById.hook");
|
||||
ipcRenderer.removeAllListeners("Proxy.getProxys.hook");
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<!-- <coming-soon />-->
|
||||
<div class="main">
|
||||
<breadcrumb>
|
||||
<div
|
||||
class="cursor-pointer h-[36px] w-[36px] bg-[#5f3bb0] rounded text-white flex justify-center items-center"
|
||||
@click="handleOpenInsert"
|
||||
>
|
||||
<Icon icon="material-symbols:add" />
|
||||
</div>
|
||||
</breadcrumb>
|
||||
<div class="app-container-breadcrumb" v-loading="loading.list > 0">
|
||||
<template v-if="proxys && proxys.length > 0">
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="proxy in proxys"
|
||||
:key="proxy._id"
|
||||
:lg="6"
|
||||
:md="8"
|
||||
:sm="12"
|
||||
:xl="6"
|
||||
:xs="24"
|
||||
class="mb-[20px]"
|
||||
>
|
||||
<div class="bg-white w-full rounded drop-shadow-xl p-4">
|
||||
<div class="w-full flex justify-between">
|
||||
<div class="flex">
|
||||
<div
|
||||
class="w-12 h-12 rounded mr-4 flex justify-center items-center"
|
||||
:class="proxy.type"
|
||||
>
|
||||
<span class="text-white text-sm">{{ proxy.type }}</span>
|
||||
</div>
|
||||
<div class="h-12 relative">
|
||||
<div class="text-sm font-bold">{{ proxy.name }}</div>
|
||||
<!-- <el-tag-->
|
||||
<!-- size="small"-->
|
||||
<!-- class="absolute bottom-0"-->
|
||||
<!-- type="success"-->
|
||||
<!-- effect="plain"-->
|
||||
<!-- >正常-->
|
||||
<!-- </el-tag>-->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-dropdown size="small">
|
||||
<a
|
||||
href="#"
|
||||
class="text-xl text-[#ADADAD] hover:text-[#5A3DAA]"
|
||||
>
|
||||
<Icon icon="material-symbols:more-vert" />
|
||||
</a>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleOpenUpdate(proxy)">
|
||||
<Icon
|
||||
icon="material-symbols:edit"
|
||||
class="primary-text text-[14px]"
|
||||
/>
|
||||
<span class="ml-1">修 改</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleDeleteProxy(proxy)">
|
||||
<Icon
|
||||
icon="material-symbols:delete-rounded"
|
||||
class="text-red-500 text-[14px]"
|
||||
/>
|
||||
<span class="ml-1">删 除</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-4">
|
||||
<div class="text-sm text-left">
|
||||
<p class="text-[#ADADAD] font-bold">内网地址</p>
|
||||
<p>{{ proxy.localIp }}</p>
|
||||
</div>
|
||||
<div class="text-sm text-center" v-if="proxy.type === 'tcp'">
|
||||
<p class="text-[#ADADAD] font-bold">外网端口</p>
|
||||
<p>{{ proxy.remotePort }}</p>
|
||||
</div>
|
||||
<div class="text-sm text-center">
|
||||
<p class="text-[#ADADAD] font-bold">内网端口</p>
|
||||
<p>{{ proxy.localPort }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="text-sm text-[#ADADAD] py-2">本地地址 本地端口</div>-->
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full bg-white rounded p-2 overflow-hidden drop-shadow-xl flex justify-center items-center"
|
||||
>
|
||||
<el-empty description="暂无代理" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="edit.visible"
|
||||
:title="edit.title"
|
||||
class="w-[400px]"
|
||||
top="30px"
|
||||
>
|
||||
<el-form
|
||||
v-loading="loading.form"
|
||||
label-position="top"
|
||||
:model="editForm"
|
||||
:rules="editFormRules"
|
||||
ref="editFormRef"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="代理类型:" prop="proxyType">
|
||||
<el-radio-group v-model="editForm.type">
|
||||
<el-radio label="http" model-value="http" />
|
||||
<el-radio label="https" model-value="https" />
|
||||
<el-radio label="tcp" model-value="tcp" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="代理名称:" prop="proxyName">
|
||||
<el-input v-model="editForm.name" placeholder="代理名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item label="内网地址:" prop="localIp">
|
||||
<el-input v-model="editForm.localIp" placeholder="127.0.0.1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="内网端口:" prop="localPort">
|
||||
<el-input-number
|
||||
placeholder="8080"
|
||||
class="!w-full"
|
||||
:min="0"
|
||||
:max="65535"
|
||||
v-model="editForm.localPort"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<template v-if="editForm.type === 'tcp'">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="外网端口:" prop="remotePort">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
:max="65535"
|
||||
placeholder="8080"
|
||||
v-model="editForm.remotePort"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
<template
|
||||
v-if="editForm.type === 'http' || editForm.type === 'https'"
|
||||
>
|
||||
<el-col :span="24">
|
||||
<el-form-item
|
||||
v-for="(d, di) in editForm.customDomains"
|
||||
:key="'domain' + di"
|
||||
:label="di === 0 ? '自定义域名:' : ''"
|
||||
:prop="`customDomains.${di}`"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: `自定义域名不能为空`,
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/,
|
||||
message: '请输入正确的域名',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<el-input
|
||||
class="domain-input"
|
||||
placeholder="github.com"
|
||||
v-model="editForm.customDomains[di]"
|
||||
/>
|
||||
<!-- <div class="domain-input-button !bg-[#67c23a]">-->
|
||||
<!-- <Icon icon="material-symbols:add" />-->
|
||||
<!-- </div>-->
|
||||
<el-button
|
||||
class="ml-[10px]"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleAddDomain"
|
||||
>
|
||||
<Icon icon="material-symbols:add" />
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDeleteDomain(di)"
|
||||
:disabled="editForm.customDomains.length === 1"
|
||||
>
|
||||
<Icon icon="material-symbols:delete-rounded" />
|
||||
</el-button>
|
||||
<!-- <div class="domain-input-button !bg-[#d3585b]">-->
|
||||
<!-- <Icon icon="material-symbols:delete-rounded" />-->
|
||||
<!-- </div>-->
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col :span="24">
|
||||
<el-form-item>
|
||||
<div class="w-full flex justify-end">
|
||||
<el-button @click="edit.visible = false">关 闭</el-button>
|
||||
<el-button plain type="primary" @click="handleSubmit"
|
||||
>保 存
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.http {
|
||||
background: #d3585b;
|
||||
}
|
||||
|
||||
.tcp {
|
||||
background: #7bbc71;
|
||||
}
|
||||
|
||||
.https {
|
||||
background: #5f3bb0;
|
||||
}
|
||||
|
||||
.domain-input {
|
||||
width: calc(100% - 115px);
|
||||
}
|
||||
|
||||
.domain-input-button {
|
||||
background: #5f3bb0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
20
tailwind.config.js
Normal file
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
49
tsconfig.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
105
vite.config.ts
Normal file
@ -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
|
||||
};
|
||||
});
|