在线终端(一个基于 Spring Boot 的在线终端模拟器,实现了类 Linux 命令行操作功能)

发布于:2025-04-17 ⋅ 阅读:(28) ⋅ 点赞:(0)

Online Terminal

一个基于 Spring Boot 的在线终端模拟器,实现了类 Linux 命令行操作功能。

功能特点

  • 模拟 Linux 文件系统操作
  • 支持基础的文件和目录管理命令
  • 提供文件内容查看和编辑功能
  • 支持文件压缩和解压缩操作

快速开始

环境要求

  • JDK 8+
  • Maven 3.6+

运行项目

  1. 克隆项目到本地
git clone https://gitee.com/anxwefndu/online-terminal.git
  1. 修改配置文件
    编辑 src/main/resources/application.properties, 设置根目录路径:
root.path=D:/linux/root/
  1. 启动项目
mvn spring-boot:run
  1. 访问地址: 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

注意事项

  1. 所有文件操作都被限制在配置的根目录下
  2. 为了安全考虑,不支持 .. 路径
  3. 文件路径支持相对路径和绝对路径

后续开发说明

目前系统还有部分功能未开发完成,等待后续完善

代码下载

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">
        <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.测试部分命令
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到