Online Terminal
一个基于 Spring Boot 的在线终端模拟器,实现了类 Linux 命令行操作功能。
功能特点
- 模拟 Linux 文件系统操作
- 支持基础的文件和目录管理命令
- 提供文件内容查看和编辑功能
- 支持文件压缩和解压缩操作
快速开始
环境要求
- JDK 8+
- Maven 3.6+
运行项目
- 克隆项目到本地
git clone https://gitee.com/anxwefndu/online-terminal.git
- 修改配置文件
编辑src/main/resources/application.properties
, 设置根目录路径:
root.path=D:/linux/root/
- 启动项目
mvn spring-boot:run
- 访问地址:
http://localhost:8080
支持的命令
文件操作命令
ls - 列出目录内容
# 基本用法
ls# 显示详细信息
ls -l# 显示隐藏文件
ls -a# 组合使用
ls -la /path/to/directory
cd - 切换目录
# 切换到指定目录
cd /path/to/directory# 返回上级目录
cd ..# 返回根目录
cd /
pwd - 显示当前工作目录
pwd
mkdir - 创建目录
# 创建单个目录
mkdir directory# 创建多级目录
mkdir -p path/to/directory
rm - 删除文件或目录
# 删除文件
rm file.txt# 递归删除目录
rm -r directory# 强制删除
rm -f file.txt# 递归强制删除目录
rm -rf directory
文件内容操作
cat - 查看文件内容
# 查看单个文件
cat file.txt# 显示行号
cat -n file.txt# 查看多个文件
cat file1.txt file2.txt
more - 分页显示文件内容
# 基本用法
more file.txt# 继续查看下一页
more -n file.txt
head - 查看文件开头
# 默认显示前10行
head file.txt# 指定显示行数
head -n 5 file.txt
tail - 查看文件结尾
# 默认显示最后10行
tail file.txt# 指定显示行数
tail -n 5 file.txt
vim - 文本编辑器
# 打开文件
vim file.txt# 编辑命令
:edit <content> # 编辑内容
:w # 保存
:q # 退出
:wq # 保存并退出
文件查找
find - 查找文件
# 按名称查找
find . -name "*.txt"# 按类型查找(f:文件,d:目录)
find . -type f
find . -type d# 在指定目录下查找
find /path/to/directory -name "*.txt"
grep - 搜索文件内容
# 基本搜索
grep "pattern" file.txt# 显示行号
grep -n "pattern" file.txt# 忽略大小写
grep -i "pattern" file.txt# 搜索多个文件
grep "pattern" file1.txt file2.txt
文件传输
cp - 复制文件
# 复制文件
cp source.txt target.txt# 递归复制目录
cp -r source_dir target_dir# 强制覆盖
cp -f source.txt target.txt
mv - 移动文件
# 移动文件
mv source.txt target/# 重命名文件
mv old.txt new.txt# 强制覆盖
mv -f source.txt target.txt
压缩文件操作
zip - 压缩文件
# 压缩文件
zip archive.zip file.txt# 压缩目录
zip -r archive.zip directory/
unzip - 解压缩文件
# 解压到当前目录
unzip archive.zip# 解压到指定目录
unzip archive.zip -d /target/directory
注意事项
- 所有文件操作都被限制在配置的根目录下
- 为了安全考虑,不支持
..
路径 - 文件路径支持相对路径和绝对路径
后续开发说明
目前系统还有部分功能未开发完成,等待后续完善
代码下载
Online Terminal
核心源码
源码/src/main/resources/static/index.html
<!DOCTYPE html>
<html lang="zh"><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>在线终端</title><script src="https://cdn.tailwindcss.com"></script><link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"><script>tailwind.config = {theme: {extend: {colors: {primary: '#2563eb',secondary: '#475569',terminal: {bg: '#1a1b26',text: '#a9b1d6',prompt: '#7aa2f7',success: '#9ece6a',error: '#f7768e'}},borderRadius: {'none': '0px','sm': '2px',DEFAULT: '4px','md': '8px','lg': '12px','xl': '16px','2xl': '20px','3xl': '24px','full': '9999px','button': '4px'}}}}</script><style>@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap');* {font-family: 'JetBrains Mono', monospace;}.file-tree::-webkit-scrollbar {width: 8px;}.file-tree::-webkit-scrollbar-track {background: #1a1b26;}.file-tree::-webkit-scrollbar-thumb {background: #414868;border-radius: 4px;}.terminal-container::-webkit-scrollbar {width: 8px;}.terminal-container::-webkit-scrollbar-track {background: #1a1b26;}.terminal-container::-webkit-scrollbar-thumb {background: #414868;border-radius: 4px;}.terminal-container {display: flex;flex-direction: column;height: 100%;font-family: 'Consolas', monospace;}.terminal-output {overflow-y: auto;}.command-input-container {position: relative;}.prompt {position: absolute;left: 0;top: 0;}.command-input-wrapper {background: transparent;border: none;color: #a9b1d6;font-family: inherit;font-size: inherit;outline: none;padding: 0;margin: 0;min-height: 1.5em;white-space: pre-wrap;word-break: break-all;}</style>
</head><body class="bg-terminal-bg text-terminal-text"><div class="max-w-[1440px] mx-auto h-screen flex flex-col"><header class="h-12 bg-terminal-bg border-b border-gray-700 flex items-center px-4 justify-between"><div class="flex items-center space-x-4"><buttonclass="text-terminal-text hover:text-terminal-prompt !rounded-button px-3 py-1.5 flex items-center space-x-2"><i class="fas fa-upload text-sm"></i><span class="text-sm whitespace-nowrap">上传文件</span></button><buttonclass="text-terminal-text hover:text-terminal-prompt !rounded-button px-3 py-1.5 flex items-center space-x-2"><i class="fas fa-download text-sm"></i><span class="text-sm whitespace-nowrap">下载文件</span></button></div><div class="flex items-center space-x-4"><button class="text-terminal-text hover:text-terminal-prompt !rounded-button px-3 py-1.5"><i class="fas fa-moon text-sm"></i></button><div class="flex items-center space-x-2"><button class="text-terminal-text hover:text-terminal-prompt !rounded-button px-2 py-1"><i class="fas fa-minus text-sm"></i></button><span class="text-sm">14px</span><button class="text-terminal-text hover:text-terminal-prompt !rounded-button px-2 py-1"><i class="fas fa-plus text-sm"></i></button></div></div></header><div class="flex-1 flex" style="overflow: hidden"><aside class="w-64 border-r border-gray-700 flex flex-col"><div class="p-4 border-b border-gray-700"><div class="text-sm text-terminal-prompt current-path">/</div></div><div class="flex-1 overflow-auto file-tree p-2"><div class="space-y-1"></div></div></aside><main class="flex-1 flex flex-col"><div class="flex-1 overflow-auto terminal-content p-4 terminal-container" onclick="focusCommandInput()"><div style="height: fit-content"><!-- 终端输出区 --><div class="terminal-output"></div><!-- 命令输入区 --><div class="command-input-container"><span class="text-terminal-prompt mr-2 prompt">user@localhost$</span><div class="command-input-wrapper" contenteditable="true"onkeydown="handleKeyDown(event)"></div></div></div></div><div class="h-12 border-t border-gray-700 flex items-center px-4 justify-between"><div class="flex items-center space-x-4"><span class="text-sm text-terminal-success"><i class="fas fa-check-circle mr-1"></i>就绪</span></div><div class="flex items-center space-x-4"></div></div></main></div></div><script src="./init.js"></script><script src="./handleFocus.js"></script><script src="./handleAppendTerminal.js"></script><script src="./handlePromote.js"></script><script src="./handleCommandInput.js"></script>
</body></html>
源码/src/main/resources/static/handleCommandInput.js
let commandHistory = [];
let historyIndex = -1;async function handleKeyDown(event) {const inputDiv = event.target;if (event.key === 'Enter') {event.preventDefault();await executeCommand();} else if (event.key === 'ArrowUp') {if (historyIndex < commandHistory.length - 1) {event.preventDefault();historyIndex++;inputDiv.textContent = commandHistory[historyIndex];// 将光标移到末尾const range = document.createRange();const sel = window.getSelection();range.selectNodeContents(inputDiv);range.collapse(false);sel.removeAllRanges();sel.addRange(range);}} else if (event.key === 'ArrowDown') {if (historyIndex > -1) {event.preventDefault();historyIndex--;inputDiv.textContent = historyIndex === -1 ? '' : commandHistory[historyIndex];// 将光标移到末尾const range = document.createRange();const sel = window.getSelection();range.selectNodeContents(inputDiv);range.collapse(false);sel.removeAllRanges();sel.addRange(range);}}
}async function executeCommand() {const inputWrapperList = document.getElementsByClassName('command-input-wrapper');const inputDiv = inputWrapperList[inputWrapperList.length - 1];const command = inputDiv.textContent.trim();const promptList = document.getElementsByClassName('prompt');const prompt = promptList[promptList.length - 1];if (command) {commandHistory.unshift(command);historyIndex = -1;try {const response = await fetch('/api/terminal/execute', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ command })});const result = await response.text();appendToTerminal(`${prompt.textContent} ${command}\n${result}`);// 如果是 cd 命令,更新提示符if (command.startsWith('cd ')) {const currentPath = await getCurrentPath();updatePrompt(currentPath);}// 更新根目录子文件及子文件夹展示await loadRootDirectory();} catch (error) {appendToTerminal(`执行出错: ${error.message}`);}inputDiv.textContent = '';} else {appendToTerminal(`${prompt.textContent}\n`);}
}// 获取当前路径
async function getCurrentPath() {try {const response = await fetch('/api/terminal/execute', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ command: 'pwd' })});const path = await response.text();return path.trim();} catch (error) {console.error('获取当前路径失败:', error);return '/';}
}// 在目录变化后更新当前目录
function updatePrompt(newPath) {const promptList = document.getElementsByClassName('prompt');const prompt = promptList[promptList.length - 1];prompt.textContent = `user@localhost$`;if (newPath === "") {newPath = "/";}if (!newPath.startsWith("/")) {newPath = "/" + newPath;}document.getElementsByClassName("current-path")[0].textContent = newPath;updatePromptIndent();
}
运行截图
1.系统首页
2.测试部分命令