├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── image ├── 主界面.png ├── 实时日志.png ├── 日志搜索1.png ├── 日志搜索2.png ├── 添加服务器.png └── 连接服务器.png ├── index.html ├── main.js ├── package-lock.json ├── package.json ├── preload.js ├── src ├── App.jsx ├── index.css └── main.jsx └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Dependency directories 12 | node_modules/ 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional eslint cache 18 | .eslintcache 19 | 20 | # Optional REPL history 21 | .node_repl_history 22 | 23 | # Output of 'npm pack' 24 | *.tgz 25 | 26 | # Yarn Integrity file 27 | yarn.lock 28 | 29 | # dotenv environment variables file 30 | .env 31 | 32 | # MacOS 33 | .DS_Store 34 | 35 | # Electron 36 | /dist/ 37 | /out/ 38 | 39 | # Build directories 40 | build/ 41 | 42 | # Coverage directory used by tools like istanbul 43 | coverage/ 44 | 45 | # TypeScript cache 46 | *.tsbuildinfo 47 | 48 | # Optional stylelint cache 49 | .stylelintcache 50 | 51 | # IDE specific files 52 | .idea/ 53 | .vscode/ 54 | *.sublime-workspace 55 | *.sublime-project 56 | 57 | # Misc 58 | *.swp 59 | *.bak 60 | *.tmp 61 | *.temp -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ 3 | ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ 4 | sass_binary_site=https://npmmirror.com/mirrors/node-sass/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 感谢您对远程服务器日志查询工具的关注!我们非常欢迎社区成员参与项目的开发和改进。本文档将指导您如何为项目做出贡献。 4 | 5 | ## 开发环境设置 6 | 7 | 1. 克隆项目到本地: 8 | 9 | ```bash 10 | git clone https://github.com/yourusername/remote-log-viewer.git 11 | cd remote-log-viewer 12 | ``` 13 | 14 | 2. 安装依赖: 15 | 16 | ```bash 17 | npm install 18 | ``` 19 | 20 | 3. 启动开发服务器: 21 | ```bash 22 | npm run dev 23 | ``` 24 | 25 | ## 打包说明 26 | 27 | ### Windows 安装程序打包 28 | 29 | 1. 安装打包依赖: 30 | 31 | ```bash 32 | npm install electron-builder --save-dev 33 | ``` 34 | 35 | 2. 在 `package.json` 中添加打包配置: 36 | 37 | ```json 38 | { 39 | "build": { 40 | "appId": "com.logcat.app", 41 | "productName": "远程服务器日志查询工具", 42 | "win": { 43 | "target": [ 44 | { 45 | "target": "nsis", 46 | "arch": ["x64"] 47 | } 48 | ], 49 | "icon": "build/icon.ico" 50 | }, 51 | "nsis": { 52 | "oneClick": false, 53 | "allowToChangeInstallationDirectory": true, 54 | "createDesktopShortcut": true, 55 | "createStartMenuShortcut": true, 56 | "shortcutName": "远程服务器日志查询工具" 57 | } 58 | }, 59 | "scripts": { 60 | "build:win": "electron-builder --win --x64" 61 | } 62 | } 63 | ``` 64 | 65 | 3. 执行打包命令: 66 | 67 | ```bash 68 | npm run build:win 69 | ``` 70 | 71 | 4. 打包后的安装程序将在 `dist` 目录下生成。 72 | 73 | ### 注意事项 74 | 75 | - 确保已安装 Node.js 和 npm 76 | - Windows 打包需要在 Windows 系统下进行 77 | - 打包前请确保所有依赖都已正确安装 78 | - 建议使用 Node.js LTS 版本 79 | - 如遇到签名相关错误,可添加 `--publish=never` 参数 80 | 81 | ## 提交代码 82 | 83 | 1. 创建新的分支: 84 | 85 | ```bash 86 | git checkout -b feature/your-feature-name 87 | ``` 88 | 89 | 2. 提交您的更改: 90 | 91 | ```bash 92 | git add . 93 | git commit -m "feat: 添加新功能" 94 | ``` 95 | 96 | 3. 推送到远程仓库: 97 | 98 | ```bash 99 | git push origin feature/your-feature-name 100 | ``` 101 | 102 | 4. 创建 Pull Request 103 | 104 | ## 提交规范 105 | 106 | 我们使用 [Conventional Commits](https://www.conventionalcommits.org/zh-hans/) 规范,提交信息格式如下: 107 | 108 | ``` 109 | (): 110 | 111 | [optional body] 112 | 113 | [optional footer] 114 | ``` 115 | 116 | 常用的 type 类型: 117 | 118 | - feat: 新功能 119 | - fix: 修复问题 120 | - docs: 文档修改 121 | - style: 代码格式修改 122 | - refactor: 代码重构 123 | - test: 测试用例修改 124 | - chore: 其他修改 125 | 126 | ## 代码规范 127 | 128 | - 遵循 ESLint 配置的代码规范 129 | - 保持代码简洁清晰 130 | - 添加必要的注释 131 | - 确保代码可以正常运行 132 | 133 | ## 问题反馈 134 | 135 | 如果您发现了问题或有新的想法,欢迎创建 Issue。在创建 Issue 时,请: 136 | 137 | 1. 使用清晰的标题 138 | 2. 详细描述问题或建议 139 | 3. 如果是 bug,请提供: 140 | - 问题的复现步骤 141 | - 期望的结果 142 | - 实际的结果 143 | - 错误信息(如果有) 144 | - 运行环境信息 145 | 146 | ## 联系我们 147 | 148 | 如果您有任何问题,可以: 149 | 150 | - 创建 Issue 151 | - 发送邮件至:[codecoming@163.com] 152 | - 通过项目的讨论区交流 153 | 154 | 感谢您的贡献! 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 远程服务器日志查询工具 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 远程服务器日志查询工具 2 | 3 | 欢迎使用 **远程服务器日志查询工具**,一个强大且高效的工具,专为开发者和系统管理员设计,帮助您轻松管理和监控远程服务器日志。 4 | 5 | ## 功能特性 6 | 7 | - **实时日志查看**:通过 SSH 连接,实时查看远程服务器的日志输出,帮助您快速定位问题。 8 | ![实时日志](./image/实时日志.png) 9 | 10 | - **日志搜索**:支持关键字搜索功能,快速查找日志中的相关信息。 11 | ![日志搜索1](./image/日志搜索1.png) 12 | ![日志搜索2](./image/日志搜索2.png) 13 | 14 | - **多服务器管理**:轻松添加、编辑和删除服务器配置,支持多服务器切换。 15 | ![添加服务器](./image/添加服务器.png) 16 | 17 | - **直观的用户界面**:基于 Ant Design 的现代化 UI,提供流畅的用户体验。 18 | ![主界面](./image/主界面.png) 19 | 20 | - **安全可靠**:通过 Electron 和 SSH 协议,确保数据传输的安全性。 21 | ![连接服务器](./image/连接服务器.png) 22 | 23 | ## 快速开始 24 | 25 | ### 环境要求 26 | 27 | - Node.js 14.x 或更高版本 28 | - npm 6.x 或更高版本 29 | 30 | ### 安装 31 | 32 | #### 开发环境 33 | 34 | 1. 克隆本仓库到本地: 35 | 36 | ```bash 37 | git clone https://github.com/123xiao/remote-log-viewer.git 38 | cd remote-log-viewer 39 | ``` 40 | 41 | 2. 安装依赖: 42 | 43 | ```bash 44 | npm install 45 | ``` 46 | 47 | 3. 启动应用: 48 | 49 | ```bash 50 | npm preview 51 | ``` 52 | 53 | #### Windows 安装包 54 | 55 | 1. 构建 Windows 安装包: 56 | 57 | ```bash 58 | npm run build:win 59 | ``` 60 | 61 | 2. 构建完成后,可以在 `dist` 目录下找到生成的安装包文件(.exe)。 62 | 63 | 3. 双击安装包文件,按照安装向导进行安装。 64 | 65 | 安装包特点: 66 | 67 | - 支持自定义安装目录 68 | - 自动创建桌面快捷方式 69 | - 添加开始菜单项 70 | - 支持卸载程序 71 | 72 | ## 使用指南 73 | 74 | 1. 启动应用后,点击"添加服务器"按钮,输入服务器的相关信息。 75 | 2. 在服务器列表中,选择一个服务器并点击"连接"按钮以建立 SSH 连接。 76 | 3. 使用"实时日志"按钮查看实时日志输出,或使用"搜索日志"功能查找特定信息。 77 | 78 | ## 贡献 79 | 80 | 欢迎贡献代码和建议!请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解更多信息。 81 | 82 | ## 许可证 83 | 84 | 本项目采用 MIT 许可证。详情请参阅 [LICENSE](LICENSE)。 85 | 86 | ## 联系我们 87 | 88 | 如有任何问题或建议,请通过 [GitHub Issues](https://github.com/123xiao/remote-log-viewer/issues) 联系我们。 89 | 90 | --- 91 | 92 | **远程服务器日志查询工具**,让您的服务器管理更轻松、更高效! 93 | 94 | ## Star History 95 | 96 | [![Star History Chart](https://api.star-history.com/svg?repos=123xiao/remote-log-viewer&type=Date)](https://www.star-history.com/#123xiao/remote-log-viewer&Date) 97 | -------------------------------------------------------------------------------- /image/主界面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/123xiao/remote-log-viewer/bdd772ecbff0708091145310cfe859b9690965af/image/主界面.png -------------------------------------------------------------------------------- /image/实时日志.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/123xiao/remote-log-viewer/bdd772ecbff0708091145310cfe859b9690965af/image/实时日志.png -------------------------------------------------------------------------------- /image/日志搜索1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/123xiao/remote-log-viewer/bdd772ecbff0708091145310cfe859b9690965af/image/日志搜索1.png -------------------------------------------------------------------------------- /image/日志搜索2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/123xiao/remote-log-viewer/bdd772ecbff0708091145310cfe859b9690965af/image/日志搜索2.png -------------------------------------------------------------------------------- /image/添加服务器.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/123xiao/remote-log-viewer/bdd772ecbff0708091145310cfe859b9690965af/image/添加服务器.png -------------------------------------------------------------------------------- /image/连接服务器.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/123xiao/remote-log-viewer/bdd772ecbff0708091145310cfe859b9690965af/image/连接服务器.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 远程服务器日志查询工具 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain } = require("electron"); 2 | const path = require("path"); 3 | const Store = require("electron-store"); 4 | const { Client } = require("ssh2"); 5 | const store = new Store(); 6 | 7 | let activeSSHConnection = null; 8 | 9 | let mainWindow; 10 | 11 | function createWindow() { 12 | mainWindow = new BrowserWindow({ 13 | webPreferences: { 14 | nodeIntegration: true, 15 | contextIsolation: true, 16 | enableRemoteModule: false, 17 | sandbox: false, 18 | webSecurity: false, 19 | preload: path.join(app.getAppPath(), "preload.js"), 20 | }, 21 | }); 22 | mainWindow.maximize(); 23 | //mainWindow.show(); 24 | 25 | // 开发环境下连接到Vite开发服务器 26 | if (process.env.NODE_ENV === "development") { 27 | console.log("Running in development mode"); 28 | console.log("Loading URL:", "http://localhost:5173"); 29 | mainWindow.loadURL("http://localhost:5173"); 30 | mainWindow.webContents.openDevTools(); 31 | } else { 32 | console.log("Running in production mode"); 33 | mainWindow.loadFile(path.join(__dirname, "dist/index.html")); 34 | } 35 | } 36 | 37 | app.whenReady().then(createWindow); 38 | 39 | app.on("window-all-closed", () => { 40 | if (process.platform !== "darwin") { 41 | app.quit(); 42 | } 43 | }); 44 | 45 | app.on("activate", () => { 46 | if (BrowserWindow.getAllWindows().length === 0) { 47 | createWindow(); 48 | } 49 | }); 50 | 51 | // 处理服务器配置的存储 52 | ipcMain.handle("saveServerConfig", async (event, config) => { 53 | const servers = store.get("servers", []); 54 | servers.push(config); 55 | store.set("servers", servers); 56 | return servers; 57 | }); 58 | 59 | ipcMain.handle("getServerConfigs", async () => { 60 | return store.get("servers", []); 61 | }); 62 | 63 | ipcMain.handle("deleteServerConfig", async (event, id) => { 64 | const servers = store.get("servers", []); 65 | const updatedServers = servers.filter((server) => server.id !== id); 66 | store.set("servers", updatedServers); 67 | return updatedServers; 68 | }); 69 | 70 | ipcMain.handle("updateServerConfig", async (event, config) => { 71 | const servers = store.get("servers", []); 72 | const index = servers.findIndex((server) => server.id === config.id); 73 | if (index !== -1) { 74 | servers[index] = config; 75 | store.set("servers", servers); 76 | } 77 | return servers; 78 | }); 79 | 80 | let sshDataHandler = null; 81 | 82 | ipcMain.handle("connectSSH", async (event, server) => { 83 | if (activeSSHConnection) { 84 | activeSSHConnection.end(); 85 | activeSSHConnection = null; 86 | } 87 | 88 | if (sshDataHandler) { 89 | ipcMain.removeListener("ssh-data", sshDataHandler); 90 | sshDataHandler = null; 91 | } 92 | 93 | return new Promise((resolve, reject) => { 94 | const conn = new Client(); 95 | 96 | conn.on("ready", () => { 97 | activeSSHConnection = conn; 98 | conn.shell((err, stream) => { 99 | if (err) { 100 | reject(err.message); 101 | return; 102 | } 103 | 104 | stream.on("data", (data) => { 105 | event.sender.send("ssh-data", data.toString()); 106 | }); 107 | 108 | stream.on("close", () => { 109 | event.sender.send("ssh-closed"); 110 | if (sshDataHandler) { 111 | ipcMain.removeListener("ssh-data", sshDataHandler); 112 | sshDataHandler = null; 113 | } 114 | activeSSHConnection = null; 115 | }); 116 | 117 | sshDataHandler = (event, data) => { 118 | if (stream && !stream.destroyed) { 119 | stream.write(data); 120 | } 121 | }; 122 | ipcMain.on("ssh-data", sshDataHandler); 123 | 124 | resolve("connected"); 125 | }); 126 | }); 127 | 128 | conn.on("error", (err) => { 129 | reject(err.message); 130 | }); 131 | 132 | conn.connect({ 133 | host: server.host, 134 | port: parseInt(server.port) || 22, 135 | username: server.username, 136 | password: server.password, 137 | }); 138 | }); 139 | }); 140 | 141 | ipcMain.handle("disconnectSSH", async () => { 142 | if (activeSSHConnection) { 143 | // 确保清理所有事件监听器 144 | if (sshDataHandler) { 145 | ipcMain.removeListener("ssh-data", sshDataHandler); 146 | sshDataHandler = null; 147 | } 148 | // 强制结束SSH连接 149 | activeSSHConnection.end(); 150 | activeSSHConnection.destroy(); 151 | activeSSHConnection = null; 152 | } 153 | return true; 154 | }); 155 | 156 | ipcMain.handle("sendSSHData", async (event, data) => { 157 | if (!activeSSHConnection) { 158 | event.sender.send("ssh-data", "Error: No active SSH connection\n"); 159 | return; 160 | } 161 | 162 | try { 163 | if (sshDataHandler) { 164 | sshDataHandler(event, data); 165 | } else { 166 | event.sender.send("ssh-data", "Error: SSH stream is not available\n"); 167 | } 168 | } catch (error) { 169 | console.error("Error writing to SSH connection:", error); 170 | event.sender.send( 171 | "ssh-data", 172 | "Error: Failed to send data to SSH connection\n" 173 | ); 174 | 175 | // 如果发生错误,尝试清理连接 176 | if (activeSSHConnection) { 177 | activeSSHConnection.end(); 178 | activeSSHConnection = null; 179 | } 180 | if (sshDataHandler) { 181 | ipcMain.removeListener("ssh-data", sshDataHandler); 182 | sshDataHandler = null; 183 | } 184 | } 185 | }); 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logcat", 3 | "version": "1.0.0", 4 | "description": "远程服务器日志查询工具", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "npm run build && npm start", 11 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 12 | "build:win": "vite build && electron-builder --win --x64" 13 | }, 14 | "dependencies": { 15 | "@ant-design/icons": "^5.2.6", 16 | "antd": "^5.12.2", 17 | "electron-store": "^8.1.0", 18 | "node-ssh": "^13.2.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-router-dom": "^6.21.0", 22 | "ssh2": "^1.16.0", 23 | "xterm": "^5.3.0", 24 | "xterm-addon-fit": "^0.8.0", 25 | "xterm-addon-web-links": "^0.9.0" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.2.43", 29 | "@types/react-dom": "^18.2.17", 30 | "@vitejs/plugin-react": "^4.2.1", 31 | "electron": "^28.3.3", 32 | "electron-builder": "^25.1.8", 33 | "eslint": "^8.55.0", 34 | "eslint-plugin-react": "^7.33.2", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "eslint-plugin-react-refresh": "^0.4.5", 37 | "vite": "^5.0.8" 38 | }, 39 | "build": { 40 | "appId": "com.logcat.app", 41 | "productName": "远程服务器日志查询工具", 42 | "directories": { 43 | "output": "dist" 44 | }, 45 | "files": [ 46 | "dist/**/*", 47 | "main.js", 48 | "preload.js", 49 | "package.json", 50 | "node_modules/**/*" 51 | ], 52 | "win": { 53 | "target": [ 54 | { 55 | "target": "nsis", 56 | "arch": [ 57 | "x64" 58 | ] 59 | } 60 | ], 61 | "icon": "build/icon.ico" 62 | }, 63 | "nsis": { 64 | "oneClick": false, 65 | "allowToChangeInstallationDirectory": true, 66 | "createDesktopShortcut": true, 67 | "createStartMenuShortcut": true, 68 | "shortcutName": "远程服务器日志查询工具", 69 | "installerIcon": "build/icon.ico", 70 | "uninstallerIcon": "build/icon.ico" 71 | }, 72 | "electronDownload": { 73 | "mirror": "https://npmmirror.com/mirrors/electron/" 74 | }, 75 | "electronVersion": "28.3.3", 76 | "publish": null 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron'); 2 | const NodeSSH = require('node-ssh'); 3 | 4 | process.once('loaded', () => { 5 | global.process = process; 6 | global.Buffer = Buffer; 7 | }); 8 | 9 | contextBridge.exposeInMainWorld('electronAPI', { 10 | saveServerConfig: (config) => ipcRenderer.invoke('saveServerConfig', config), 11 | getServerConfigs: () => ipcRenderer.invoke('getServerConfigs'), 12 | deleteServerConfig: (id) => ipcRenderer.invoke('deleteServerConfig', id), 13 | updateServerConfig: (config) => ipcRenderer.invoke('updateServerConfig', config), 14 | connectSSH: (server) => ipcRenderer.invoke('connectSSH', server), 15 | disconnectSSH: () => ipcRenderer.invoke('disconnectSSH'), 16 | sendSSHData: (data) => ipcRenderer.invoke('sendSSHData', data), 17 | onSSHData: (callback) => ipcRenderer.on('ssh-data', (event, data) => callback(data)), 18 | onSSHClosed: (callback) => ipcRenderer.on('ssh-closed', () => callback()) 19 | }); 20 | 21 | contextBridge.exposeInMainWorld('process', { 22 | platform: process.platform, 23 | env: { 24 | NODE_ENV: process.env.NODE_ENV 25 | } 26 | }); -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { 3 | Layout, 4 | Typography, 5 | Button, 6 | Table, 7 | Modal, 8 | Form, 9 | Input, 10 | message, 11 | Space, 12 | Card, 13 | Spin, 14 | } from "antd"; 15 | import { GithubOutlined } from "@ant-design/icons"; 16 | import { 17 | PlusOutlined, 18 | EditOutlined, 19 | DeleteOutlined, 20 | CopyOutlined, 21 | } from "@ant-design/icons"; 22 | import { Terminal } from "xterm"; 23 | import { FitAddon } from "xterm-addon-fit"; 24 | import { WebLinksAddon } from "xterm-addon-web-links"; 25 | import "xterm/css/xterm.css"; 26 | 27 | const { Header, Content, Footer } = Layout; 28 | const { Title, Text, Link } = Typography; 29 | //const { Title } = Typography; 30 | 31 | const App = () => { 32 | const [servers, setServers] = useState([]); 33 | const [isModalVisible, setIsModalVisible] = useState(false); 34 | const [editingServer, setEditingServer] = useState(null); 35 | const [form] = Form.useForm(); 36 | const [selectedServer, setSelectedServer] = useState(null); 37 | const [isConnected, setIsConnected] = useState(false); 38 | const [loading, setLoading] = useState(true); 39 | const terminalRef = useRef(null); 40 | const terminalContainerRef = useRef(null); 41 | const sshClientRef = useRef(null); 42 | 43 | useEffect(() => { 44 | const init = async () => { 45 | await loadServerConfigs(); 46 | initTerminal(); 47 | setLoading(false); 48 | }; 49 | init(); 50 | return () => { 51 | if (terminalRef.current?.cleanup) { 52 | terminalRef.current.cleanup(); 53 | } 54 | disconnectSSH(); 55 | }; 56 | }, []); 57 | 58 | const initTerminal = () => { 59 | if (terminalContainerRef.current) { 60 | if (!terminalRef.current) { 61 | const terminal = new Terminal({ 62 | cursorBlink: true, 63 | allowTransparency: true, 64 | copyOnSelect: true, 65 | rightClickSelectsWord: true, 66 | allowProposedApi: true, 67 | rightClickPaste: true, 68 | theme: { 69 | background: "#1e1e1e", 70 | foreground: "#d4d4d4", 71 | }, 72 | cols: 200, 73 | rows: 50, 74 | scrollback: 10000, 75 | fontSize: 14, 76 | fontFamily: 'Menlo, Monaco, "Courier New", monospace', 77 | lineHeight: 1.2, 78 | }); 79 | 80 | // 添加选中文本自动复制功能 81 | terminal.onSelectionChange(() => { 82 | if (terminal.hasSelection()) { 83 | const selectedText = terminal.getSelection(); 84 | navigator.clipboard.writeText(selectedText); 85 | } 86 | }); 87 | 88 | // 添加右键粘贴功能 89 | terminalContainerRef.current.addEventListener("contextmenu", (e) => { 90 | e.preventDefault(); 91 | navigator.clipboard.readText().then((text) => { 92 | terminal.paste(text); 93 | }); 94 | }); 95 | 96 | const fitAddon = new FitAddon(); 97 | terminal.loadAddon(fitAddon); 98 | terminal.loadAddon(new WebLinksAddon()); 99 | 100 | terminal.open(terminalContainerRef.current); 101 | 102 | setTimeout(() => { 103 | fitAddon.fit(); 104 | }, 0); 105 | 106 | terminalRef.current = terminal; 107 | 108 | const handleResize = () => { 109 | fitAddon.fit(); 110 | }; 111 | 112 | window.addEventListener("resize", handleResize); 113 | 114 | terminalRef.current.cleanup = () => { 115 | window.removeEventListener("resize", handleResize); 116 | terminal.dispose(); 117 | }; 118 | } 119 | } 120 | }; 121 | 122 | const loadServerConfigs = async () => { 123 | const configs = await window.electronAPI.getServerConfigs(); 124 | setServers(configs); 125 | }; 126 | 127 | const showModal = (server = null) => { 128 | setEditingServer(server); 129 | if (server) { 130 | form.setFieldsValue(server); 131 | } else { 132 | form.resetFields(); 133 | } 134 | setIsModalVisible(true); 135 | }; 136 | 137 | const handleCancel = () => { 138 | setIsModalVisible(false); 139 | form.resetFields(); 140 | setEditingServer(null); 141 | }; 142 | 143 | const handleSubmit = async () => { 144 | try { 145 | const values = await form.validateFields(); 146 | const serverConfig = { 147 | ...values, 148 | id: editingServer ? editingServer.id : Date.now().toString(), 149 | }; 150 | 151 | if (editingServer) { 152 | await window.electronAPI.updateServerConfig(serverConfig); 153 | } else { 154 | await window.electronAPI.saveServerConfig(serverConfig); 155 | } 156 | 157 | message.success(`${editingServer ? "更新" : "添加"}服务器配置成功`); 158 | loadServerConfigs(); 159 | handleCancel(); 160 | } catch (error) { 161 | message.error("表单验证失败"); 162 | } 163 | }; 164 | 165 | const handleDelete = async (id) => { 166 | try { 167 | await window.electronAPI.deleteServerConfig(id); 168 | message.success("删除服务器配置成功"); 169 | loadServerConfigs(); 170 | } catch (error) { 171 | message.error("删除失败"); 172 | } 173 | }; 174 | 175 | const connectSSH = async (server) => { 176 | if (isConnected) { 177 | await disconnectSSH(); 178 | } 179 | 180 | try { 181 | if (!terminalRef.current) { 182 | const terminal = new Terminal({ 183 | cursorBlink: true, 184 | allowTransparency: true, 185 | copyOnSelect: true, 186 | rightClickSelectsWord: true, 187 | allowProposedApi: true, 188 | rightClickPaste: true, 189 | theme: { 190 | background: "#1e1e1e", 191 | foreground: "#d4d4d4", 192 | }, 193 | cols: 200, 194 | rows: 50, 195 | scrollback: 10000, 196 | fontSize: 14, 197 | fontFamily: 'Menlo, Monaco, "Courier New", monospace', 198 | lineHeight: 1.2, 199 | convertEol: true, 200 | scrollOnOutput: true, 201 | }); 202 | 203 | const fitAddon = new FitAddon(); 204 | terminal.loadAddon(fitAddon); 205 | terminal.loadAddon(new WebLinksAddon()); 206 | 207 | terminal.open(terminalContainerRef.current); 208 | 209 | setTimeout(() => { 210 | fitAddon.fit(); 211 | }, 0); 212 | 213 | terminalRef.current = terminal; 214 | terminalRef.current.fitAddon = fitAddon; 215 | 216 | const handleResize = () => { 217 | fitAddon.fit(); 218 | }; 219 | 220 | window.addEventListener("resize", handleResize); 221 | 222 | terminalRef.current.cleanup = () => { 223 | window.removeEventListener("resize", handleResize); 224 | terminal.dispose(); 225 | }; 226 | } else { 227 | if (terminalRef.current.fitAddon) { 228 | terminalRef.current.fitAddon.fit(); 229 | } 230 | } 231 | 232 | await window.electronAPI.connectSSH(server); 233 | setIsConnected(true); 234 | setSelectedServer(server); 235 | message.success("SSH连接成功"); 236 | 237 | window.electronAPI.removeAllListeners?.("ssh-data"); 238 | window.electronAPI.removeAllListeners?.("ssh-closed"); 239 | 240 | window.electronAPI.onSSHData((data) => { 241 | if (terminalRef.current) { 242 | terminalRef.current.write(data); 243 | terminalRef.current.scrollToBottom(); 244 | } 245 | }); 246 | 247 | window.electronAPI.onSSHClosed(() => { 248 | disconnectSSH(); 249 | }); 250 | 251 | terminalRef.current?.onData((data) => { 252 | window.electronAPI.sendSSHData(data); 253 | }); 254 | } catch (error) { 255 | message.error("连接失败: " + error.message); 256 | setIsConnected(false); 257 | } 258 | }; 259 | 260 | const disconnectSSH = async () => { 261 | try { 262 | window.electronAPI.removeAllListeners?.("ssh-data"); 263 | window.electronAPI.removeAllListeners?.("ssh-closed"); 264 | 265 | await window.electronAPI.disconnectSSH(); 266 | setIsConnected(false); 267 | setSelectedServer(null); 268 | if (terminalRef.current) { 269 | terminalRef.current.clear(); 270 | terminalRef.current.dispose(); 271 | terminalRef.current = null; 272 | } 273 | message.success("已断开连接"); 274 | location.reload(); 275 | } catch (error) { 276 | message.error("断开连接失败: " + error.message); 277 | } 278 | }; 279 | 280 | const viewLiveLog = async (server) => { 281 | try { 282 | if (!isConnected || selectedServer?.id !== server.id) { 283 | await connectSSH(server); 284 | } 285 | terminalRef.current?.clear(); 286 | await window.electronAPI.sendSSHData(`tail -f ${server.logPath}\n`); 287 | } catch (error) { 288 | message.error("查看实时日志失败: " + error.message); 289 | } 290 | }; 291 | 292 | const searchLog = async (server) => { 293 | let inputRef = null; 294 | Modal.confirm({ 295 | title: "日志搜索", 296 | content: ( 297 | (inputRef = node)} 299 | placeholder="请输入搜索关键词" 300 | /> 301 | ), 302 | onOk: async () => { 303 | const keyword = inputRef?.input?.value; 304 | if (!keyword) { 305 | message.error("请输入搜索关键词"); 306 | return; 307 | } 308 | try { 309 | terminalRef.current?.clear(); 310 | 311 | if (!isConnected || selectedServer?.id !== server.id) { 312 | await connectSSH(server); 313 | } 314 | await window.electronAPI.sendSSHData( 315 | `grep -n "${keyword}" ${server.logPath}\n` 316 | ); 317 | } catch (error) { 318 | message.error("搜索日志失败: " + error.message); 319 | } 320 | }, 321 | }); 322 | }; 323 | 324 | const columns = [ 325 | { 326 | title: "服务器名称", 327 | dataIndex: "name", 328 | key: "name", 329 | }, 330 | { 331 | title: "主机地址", 332 | dataIndex: "host", 333 | key: "host", 334 | }, 335 | { 336 | title: "端口", 337 | dataIndex: "port", 338 | key: "port", 339 | }, 340 | { 341 | title: "用户名", 342 | dataIndex: "username", 343 | key: "username", 344 | }, 345 | { 346 | title: "日志路径", 347 | dataIndex: "logPath", 348 | key: "logPath", 349 | }, 350 | { 351 | title: "备注", 352 | dataIndex: "remark", 353 | key: "remark", 354 | }, 355 | { 356 | title: "操作", 357 | key: "action", 358 | render: (_, record) => ( 359 | 360 | {isConnected && selectedServer?.id === record.id ? ( 361 | 364 | ) : ( 365 | 368 | )} 369 | 372 | 375 | 382 | 389 | 390 | ), 391 | }, 392 | ]; 393 | 394 | return ( 395 | 396 |
397 |
405 | 406 | 远程服务器日志查询工具 407 | 408 | v1.0.0 409 | 作者: KK 410 |
411 |
412 | 413 | 414 |
415 | 416 | 423 | 424 | ({ 429 | onClick: () => setSelectedServer(record), 430 | })} 431 | rowClassName={(record) => 432 | record.id === selectedServer?.id ? "ant-table-row-selected" : "" 433 | } 434 | /> 435 | 436 | 437 | 446 |
454 | 481 |
482 |
498 | 519 | 520 | 521 | 528 |
529 | 534 | 535 | 536 | 541 | 542 | 543 | 549 | 550 | 551 | 556 | 557 | 558 | 563 | 564 | 565 | 570 | 571 | 572 | 573 | 574 | 575 | 576 |
577 | 578 | 579 |
580 | 581 | 582 | 一个简单易用的远程服务器日志查询工具,支持实时日志查看和关键词搜索 583 | 584 | 588 | 589 | 590 | 在GitHub上查看源码 591 | 592 | 593 | 594 |
595 | 596 | ); 597 | }; 598 | 599 | export default App; 600 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 9 | 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 10 | 'Noto Color Emoji'; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | background-color: #f0f2f5; 14 | } 15 | 16 | #root { 17 | height: 100vh; 18 | } 19 | 20 | .app-container { 21 | min-height: 100vh; 22 | display: flex; 23 | flex-direction: column; 24 | } 25 | 26 | .app-header { 27 | background: #fff; 28 | padding: 0 24px; 29 | display: flex; 30 | align-items: center; 31 | justify-content: space-between; 32 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 33 | } 34 | 35 | .app-content { 36 | flex: 1; 37 | padding: 24px; 38 | } 39 | 40 | .server-list { 41 | margin-bottom: 24px; 42 | } 43 | 44 | .log-viewer { 45 | background: #fff; 46 | padding: 24px; 47 | border-radius: 4px; 48 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 49 | } 50 | 51 | .log-content { 52 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; 53 | background: #1e1e1e; 54 | color: #d4d4d4; 55 | padding: 16px; 56 | border-radius: 4px; 57 | overflow: auto; 58 | max-height: 600px; 59 | } 60 | 61 | .highlight { 62 | background-color: #ffd700; 63 | color: #000; 64 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 4 | import { ConfigProvider } from 'antd'; 5 | import zhCN from 'antd/locale/zh_CN'; 6 | import App from './App'; 7 | import './index.css'; 8 | 9 | ReactDOM.createRoot(document.getElementById('root')).render( 10 | 11 | 12 | 13 | 14 | } /> 15 | 16 | 17 | 18 | 19 | ); -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | port: 5173, 8 | }, 9 | base: './', 10 | build: { 11 | outDir: "dist", 12 | emptyOutDir: true, 13 | rollupOptions: { 14 | external: ['electron'] 15 | } 16 | }, 17 | optimizeDeps: { 18 | exclude: ['electron'] 19 | }, 20 | }); 21 | --------------------------------------------------------------------------------