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
注意事项
- 所有文件操作都被限制在配置的根目录下
- 为了安全考虑,不支持
..
路径 - 文件路径支持相对路径和绝对路径
后续开发说明
目前系统还有部分功能未开发完成,等待后续完善
代码下载
核心源码
源码/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">
<button
class="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>
<button
class="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.测试部分命令