code-server-like
This commit is contained in:
22
code-server-like/app/__init__.py
Normal file
22
code-server-like/app/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from .views.routes import main_bp
|
||||||
|
from .sockets.terminal_service import terminal_bp
|
||||||
|
from .views.file import file_bp
|
||||||
|
from .config import Config
|
||||||
|
from .extensions import init_extensions
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__, template_folder="../templates", static_folder="../static",static_url_path="/vsc-like/static")
|
||||||
|
app.config.from_object(Config)
|
||||||
|
@app.route('/')
|
||||||
|
def hello():
|
||||||
|
return 'Hello, World!'
|
||||||
|
|
||||||
|
# 注册蓝图
|
||||||
|
app.register_blueprint(main_bp)
|
||||||
|
app.register_blueprint(terminal_bp)
|
||||||
|
app.register_blueprint(file_bp)
|
||||||
|
# 初始化SocketIO
|
||||||
|
init_extensions(app)
|
||||||
|
|
||||||
|
return app
|
||||||
16
code-server-like/app/config.py
Normal file
16
code-server-like/app/config.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
GLOBAL_CONFIG = configparser.ConfigParser()
|
||||||
|
GLOBAL_CONFIG.read('config.ini')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
SECRET_KEY = GLOBAL_CONFIG['Global']['SECRET_KEY']
|
||||||
|
SOCKETIO_MESSAGE_QUEUE = GLOBAL_CONFIG['Global'].get('SOCKETIO_MESSAGE_QUEUE', 'redis://localhost:6379/0')
|
||||||
|
CORS_HEADERS = GLOBAL_CONFIG['Global'].get('CORS_HEADERS', 'Content-Type')
|
||||||
|
|
||||||
|
# 自定义其他全局配置
|
||||||
|
ROOT_WORKSPACE_PATH = GLOBAL_CONFIG['Global']['ROOT_WORKSPACE_PATH']
|
||||||
|
|
||||||
11
code-server-like/app/extensions.py
Normal file
11
code-server-like/app/extensions.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from flask_cors import CORS
|
||||||
|
from flask_socketio import SocketIO
|
||||||
|
from .config import Config
|
||||||
|
# 初始化扩展
|
||||||
|
cors = CORS()
|
||||||
|
socketio = SocketIO()
|
||||||
|
session_paths = {}
|
||||||
|
def init_extensions(app):
|
||||||
|
cors.init_app(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
|
||||||
|
socketio.init_app(app, cors_allowed_origins="*", max_payload_size=52428800, async_mode="threading")
|
||||||
|
app.extensions["session_paths"] = session_paths
|
||||||
24
code-server-like/app/services/file_service.py
Normal file
24
code-server-like/app/services/file_service.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
# 递归获取目录文件树
|
||||||
|
def get_file_tree(path):
|
||||||
|
file_tree = []
|
||||||
|
for item in os.listdir(path):
|
||||||
|
full_path = os.path.join(path, item)
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
file_tree.append({"name": item, "type": "directory", "children": get_file_tree(full_path)})
|
||||||
|
else:
|
||||||
|
file_tree.append({"name": item, "type": "file"})
|
||||||
|
return file_tree
|
||||||
|
|
||||||
|
def load_config(path):
|
||||||
|
config_file = os.path.join(path, '.config')
|
||||||
|
if not os.path.exists(config_file):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
64
code-server-like/app/sockets/terminal_service.py
Normal file
64
code-server-like/app/sockets/terminal_service.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from flask import Blueprint, request, current_app, session
|
||||||
|
from flask_socketio import SocketIO, emit, join_room
|
||||||
|
from threading import Thread
|
||||||
|
import sys
|
||||||
|
from ..extensions import socketio
|
||||||
|
|
||||||
|
terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like')
|
||||||
|
|
||||||
|
# 存储终端进程
|
||||||
|
terminals = {}
|
||||||
|
|
||||||
|
def start_terminal_process(socket_id):
|
||||||
|
# 启动一个 bash 终端
|
||||||
|
process = subprocess.Popen(
|
||||||
|
['bash'],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 监听进程的输出
|
||||||
|
while True:
|
||||||
|
output = process.stdout.readline()
|
||||||
|
if output:
|
||||||
|
emit('terminal_output', {'data': output}, room=socket_id)
|
||||||
|
if process.poll() is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
@socketio.on('connect', namespace='/vsc-like/terminal')
|
||||||
|
def connect_terminal():
|
||||||
|
socket_id = session.get('uuid')
|
||||||
|
path = session.get('path')
|
||||||
|
if not path:
|
||||||
|
emit('error', {'message': 'Authentication failed, no path found for this session'})
|
||||||
|
return False # 拒绝连接
|
||||||
|
terminals[socket_id] = subprocess.Popen(
|
||||||
|
['bash'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||||
|
)
|
||||||
|
emit('connected', {'message': 'Terminal connected'})
|
||||||
|
join_room(path, namespace='/vsc-like/terminal')
|
||||||
|
# 启动监听终端输出的线程
|
||||||
|
thread = Thread(target=start_terminal_process, args=(socket_id,))
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
@socketio.on('send_input', namespace='/vsc-like/terminal')
|
||||||
|
def send_input(data):
|
||||||
|
socket_id = session.get('uuid')
|
||||||
|
path = session.get('path')
|
||||||
|
input_data = data.get('input')
|
||||||
|
if socket_id in terminals:
|
||||||
|
terminals[socket_id].stdin.write(input_data + '\n')
|
||||||
|
terminals[socket_id].stdin.flush()
|
||||||
|
|
||||||
|
@socketio.on('disconnect', namespace='/vsc-like/terminal')
|
||||||
|
def disconnect_terminal():
|
||||||
|
socket_id = session.get('uuid')
|
||||||
|
path = session.get('path')
|
||||||
|
if socket_id in terminals:
|
||||||
|
terminals[socket_id].terminate()
|
||||||
|
del terminals[socket_id]
|
||||||
164
code-server-like/app/views/file.py
Normal file
164
code-server-like/app/views/file.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
from hmac import new
|
||||||
|
import shutil
|
||||||
|
from flask import Blueprint, request, jsonify, current_app, render_template, session
|
||||||
|
import os
|
||||||
|
from ..services.file_service import get_file_tree
|
||||||
|
|
||||||
|
file_bp = Blueprint('file', __name__, url_prefix='/vsc-like')
|
||||||
|
|
||||||
|
@file_bp.route('/create-folder', methods=['POST'])
|
||||||
|
def create_folder():
|
||||||
|
try:
|
||||||
|
filepath = request.json.get('path', '')
|
||||||
|
path = session.get('path')
|
||||||
|
parent = request.json.get('parent', '')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
base_path = f'{root_workspace_path}/{path}'
|
||||||
|
filepath = f'{base_path}/{parent}/{filepath}'
|
||||||
|
try:
|
||||||
|
os.makedirs(filepath)
|
||||||
|
return jsonify({"message": "Folder created successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
@file_bp.route('/create-file', methods=['POST'])
|
||||||
|
def create_file():
|
||||||
|
filepath = request.json.get('path', '')
|
||||||
|
path = session.get('path')
|
||||||
|
parent = request.json.get('parent', '')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
base_path = f'{root_workspace_path}/{path}'
|
||||||
|
filepath = f'{base_path}/{parent}/{filepath}'
|
||||||
|
try:
|
||||||
|
open(filepath, 'w').close()
|
||||||
|
return jsonify({"message": "File created successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@file_bp.route('/rename-file', methods=['POST'])
|
||||||
|
def rename_file():
|
||||||
|
user_root = session.get('path')
|
||||||
|
path = request.json.get('path', '')
|
||||||
|
new_name = request.json.get('newName', '')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
base_path = f'{root_workspace_path}/{user_root}'
|
||||||
|
filepath = f'{base_path}/{path}'
|
||||||
|
try:
|
||||||
|
path_list = filepath.split('/')
|
||||||
|
path_list[-1] = new_name
|
||||||
|
new_filepath = '/'.join(path_list)
|
||||||
|
os.rename(filepath, new_filepath)
|
||||||
|
return jsonify({"message": "File renamed successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@file_bp.route('/rename-folder', methods=['POST'])
|
||||||
|
def rename_folder():
|
||||||
|
user_root = session.get('path')
|
||||||
|
path = request.json.get('path', '')
|
||||||
|
new_name = request.json.get('newName', '')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
base_path = f'{root_workspace_path}/{user_root}'
|
||||||
|
folderpath = f'{base_path}/{path}'
|
||||||
|
try:
|
||||||
|
path_list = folderpath.split('/')
|
||||||
|
path_list[-1] = new_name
|
||||||
|
new_filepath = '/'.join(path_list)
|
||||||
|
os.rename(folderpath, new_filepath)
|
||||||
|
return jsonify({"message": "Folder renamed successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@file_bp.route('/copy-files', methods=['POST'])
|
||||||
|
def copy_files():
|
||||||
|
old_path = request.json.get('oldPath', '')
|
||||||
|
new_path = request.json.get('newPath', '')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
user_root = session.get('path')
|
||||||
|
base_path = f'{root_workspace_path}/{user_root}'
|
||||||
|
old_path = f'{base_path}/{old_path}'
|
||||||
|
new_path = f'{base_path}/{new_path}'
|
||||||
|
# oldpath的父目录就是newpath的话,对oldpath进行重命名
|
||||||
|
|
||||||
|
# 获取 old_path 的文件/文件夹名
|
||||||
|
old_name = os.path.basename(old_path)
|
||||||
|
|
||||||
|
# 确保目标路径是父目录(即 new_path 是一个目录)
|
||||||
|
new_path_parent = os.path.abspath(new_path)
|
||||||
|
|
||||||
|
# 如果目标路径下已经存在同名文件/文件夹,进行重命名
|
||||||
|
final_new_path = os.path.join(new_path_parent, old_name)
|
||||||
|
counter = 1
|
||||||
|
while os.path.exists(final_new_path):
|
||||||
|
# 如果文件夹或文件已经存在,则为新的文件名添加计数后缀
|
||||||
|
final_new_path = os.path.join(new_path_parent, f"{old_name}_{counter}")
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# 判断 old_path 是文件夹还是文件,并进行相应的复制操作
|
||||||
|
if os.path.isdir(old_path):
|
||||||
|
shutil.copytree(old_path, final_new_path, dirs_exist_ok=True)
|
||||||
|
else:
|
||||||
|
shutil.copy(old_path, final_new_path)
|
||||||
|
|
||||||
|
return jsonify({"message": "Files copied successfully"})
|
||||||
|
|
||||||
|
@file_bp.route('/delete-file', methods=['POST'])
|
||||||
|
def delete_file():
|
||||||
|
path = request.json.get('path', '')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
user_root = session.get('path')
|
||||||
|
base_path = f'{root_workspace_path}/{user_root}'
|
||||||
|
filepath = f'{base_path}/{path}'
|
||||||
|
if os.path.isdir(filepath):
|
||||||
|
shutil.rmtree(filepath)
|
||||||
|
else:
|
||||||
|
os.remove(filepath)
|
||||||
|
return jsonify({"message": "File deleted successfully"})
|
||||||
|
|
||||||
|
@file_bp.route('/file-content', methods=['POST'])
|
||||||
|
def file_content():
|
||||||
|
path = session.get('path')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
base_path = f'{root_workspace_path}/{path}'
|
||||||
|
filepath = request.json.get('path', '')
|
||||||
|
filepath = f'{base_path}/{filepath}'
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
return jsonify({"error": "File not found"}), 404
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r') as file:
|
||||||
|
content = file.read()
|
||||||
|
return jsonify({"content": content})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@file_bp.route('/save-file', methods=['POST'])
|
||||||
|
def save_file():
|
||||||
|
path = session.get('path')
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
base_path = f'{root_workspace_path}/{path}'
|
||||||
|
filepath = request.json.get('path', '')
|
||||||
|
filepath = f'{base_path}/{filepath}'
|
||||||
|
content = request.json.get('content', '')
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
return jsonify({"error": "File not found"}), 404
|
||||||
|
try:
|
||||||
|
with open(filepath, 'w') as file:
|
||||||
|
file.write(content)
|
||||||
|
return jsonify({"message": "File saved successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@file_bp.route('/file-tree', methods=['GET'])
|
||||||
|
def file_tree():
|
||||||
|
path = session.get('path')
|
||||||
|
if not path:
|
||||||
|
return jsonify({"error": "Missing path parameter"}), 400
|
||||||
|
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
base_path = f'{root_workspace_path}/{path}'
|
||||||
|
if not os.path.exists(base_path):
|
||||||
|
return jsonify({"error": "Workspace not found"}), 404
|
||||||
|
tree = get_file_tree(base_path)
|
||||||
|
return jsonify(tree)
|
||||||
|
|
||||||
|
|
||||||
24
code-server-like/app/views/routes.py
Normal file
24
code-server-like/app/views/routes.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
from flask import Blueprint, request, jsonify, current_app, render_template, session
|
||||||
|
from ..services.file_service import get_file_tree, load_config
|
||||||
|
|
||||||
|
main_bp = Blueprint('main', __name__, url_prefix='/vsc-like')
|
||||||
|
|
||||||
|
@main_bp.route('/', methods=['GET'])
|
||||||
|
def vsc_like():
|
||||||
|
uuid = request.args.get('uuid')
|
||||||
|
path = request.args.get('path')
|
||||||
|
session['uuid'] = uuid
|
||||||
|
session['path'] = path
|
||||||
|
# 加载 .config 文件并验证 uuid
|
||||||
|
base_path = current_app.config['ROOT_WORKSPACE_PATH']
|
||||||
|
path = f'{base_path}/{path}'
|
||||||
|
config = load_config(path)
|
||||||
|
if not config:
|
||||||
|
return jsonify({"error": "Configuration file not found or invalid"}), 404
|
||||||
|
if config.get('uuid') != uuid:
|
||||||
|
return jsonify({"error": "UUID does not match"}), 403
|
||||||
|
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
3
code-server-like/config.ini
Normal file
3
code-server-like/config.ini
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[Global]
|
||||||
|
SECRET_KEY = cakebaker
|
||||||
|
ROOT_WORKSPACE_PATH = ../study
|
||||||
3
code-server-like/requirements.txt
Normal file
3
code-server-like/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Flask
|
||||||
|
Flask-SocketIO
|
||||||
|
python-dotenv
|
||||||
12
code-server-like/run.py
Normal file
12
code-server-like/run.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from app import create_app
|
||||||
|
from app.extensions import socketio
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
socketio.run(
|
||||||
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=5200,
|
||||||
|
debug=True, # 开发期打开
|
||||||
|
allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示
|
||||||
|
)
|
||||||
155
code-server-like/static/css/index.css
Normal file
155
code-server-like/static/css/index.css
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
|
||||||
|
/* 给按钮设置统一的样式 */
|
||||||
|
#topbar button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 12px; /* 可以调整图标的大小 */
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
color: #333;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标悬浮时改变图标的颜色 */
|
||||||
|
#topbar button:hover {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 可以根据需要设置按钮的大小和间距 */
|
||||||
|
#newFileBtn, #newFolderBtn, #toggleSearchBtn, #refreshBtn {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 默认树节点样式 */
|
||||||
|
.tree-item {
|
||||||
|
padding: 4px 8px; /* 调整左右内边距 */
|
||||||
|
margin: 2px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标悬停时的动画效果 */
|
||||||
|
.tree-item:hover {
|
||||||
|
background-color: #d3d3d3;
|
||||||
|
transform: scale(1.02); /* 略微放大 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中项的样式 */
|
||||||
|
.tree-item.selected {
|
||||||
|
background-color: #42aaef; /* 选中项背景颜色 */
|
||||||
|
color: rgb(32, 32, 32); /* 选中项字体颜色 */
|
||||||
|
border: 2px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件夹样式 */
|
||||||
|
.directory {
|
||||||
|
background-color: #e6e6e6; /* 文件夹背景色 */
|
||||||
|
color: rgb(32, 32, 32); /* 文件夹字体颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory i {
|
||||||
|
margin-right: 8px; /* 图标与文本之间的间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory:hover {
|
||||||
|
background-color: #42aaef; /* 悬停时的文件夹颜色 */
|
||||||
|
color: rgb(32, 32, 32); /* 悬停时字体变白 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件样式 */
|
||||||
|
.file {
|
||||||
|
background-color: #e6e6e6; /* 文件背景色 */
|
||||||
|
color: rgb(32, 32, 32); /* 文件字体颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.file i {
|
||||||
|
margin-right: 8px; /* 图标与文本之间的间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.file:hover {
|
||||||
|
background-color: #42aaef; /* 文件悬停时背景 */
|
||||||
|
color: rgb(32, 32, 32); /* 悬停时字体颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 处理文件夹和文件图标 */
|
||||||
|
.directory i::before {
|
||||||
|
content: "\f07b"; /* 文件夹图标 */
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file i::before {
|
||||||
|
content: "\f15b"; /* 文件图标 */
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 树节点的层级缩进 */
|
||||||
|
.tree-item-level-1 {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-level-2 {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-level-3 {
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-level-4 {
|
||||||
|
padding-left: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-level-5 {
|
||||||
|
padding-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-level-6 {
|
||||||
|
padding-left: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-level-7 {
|
||||||
|
padding-left: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 隐藏右键菜单 */
|
||||||
|
.context-menu {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
display: none; /* 默认隐藏 */
|
||||||
|
z-index: 1000;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu li {
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu li:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中状态样式 */
|
||||||
|
.context-menu .active {
|
||||||
|
background-color: #d3d3d3;
|
||||||
|
}
|
||||||
323
code-server-like/static/js/file.js
Normal file
323
code-server-like/static/js/file.js
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
let fileTree = [];
|
||||||
|
let editor = null;
|
||||||
|
let showdotfile = false;
|
||||||
|
// 隐藏右键菜单
|
||||||
|
function hideContextMenu() {
|
||||||
|
const contextMenu = document.getElementById("contextMenu");
|
||||||
|
contextMenu.style.display = "none";
|
||||||
|
}
|
||||||
|
// 加载文件树
|
||||||
|
function loadFileTree() {
|
||||||
|
fetch('/vsc-like/file-tree')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
fileTree = sortFileTree(data);
|
||||||
|
const fileTreeDiv = document.getElementById('fileTreeContent');
|
||||||
|
fileTreeDiv.innerHTML = '';
|
||||||
|
renderFileTree(fileTree, fileTreeDiv);
|
||||||
|
for (const item of fileTree) {
|
||||||
|
if (item.type === 'directory') {
|
||||||
|
// 如果选中的文件夹是当前文件夹,则显示或选中的文件处于当前文件夹(祖先)则显示
|
||||||
|
if (selectedItem && (selectedItem.path+'/'+selectedItem.name).includes(item.path+'/'+item.name)) {
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
showOrHideChildren(item.div);
|
||||||
|
showOrHideChildren(item.div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showdotfile==false) {
|
||||||
|
if (item.name.startsWith('.')) {
|
||||||
|
item.div.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bindRightClickMenu(fileTree);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let selectedItem = null; // 用来保存当前选中的文件/文件夹
|
||||||
|
let copiedItem = null; // 用于存储复制的文件或文件夹
|
||||||
|
|
||||||
|
function selectItem(item) {
|
||||||
|
if (selectedItem) {
|
||||||
|
selectedItem.div.classList.remove('selected');
|
||||||
|
}
|
||||||
|
item.div.classList.add('selected');
|
||||||
|
selectedItem = item;
|
||||||
|
}
|
||||||
|
// 右键点击文件或文件夹时显示菜单
|
||||||
|
function showContextMenu(event, item) {
|
||||||
|
event.preventDefault(); // 阻止默认右键菜单
|
||||||
|
|
||||||
|
// 记录当前右键点击的文件/文件夹
|
||||||
|
selectedItem = item;
|
||||||
|
if (selectedItem) {
|
||||||
|
selectedItem.div.classList.remove('selected');
|
||||||
|
}
|
||||||
|
item.div.classList.add('selected');
|
||||||
|
|
||||||
|
// 获取右键菜单元素
|
||||||
|
const contextMenu = document.getElementById("contextMenu");
|
||||||
|
|
||||||
|
// 设置菜单位置
|
||||||
|
contextMenu.style.top = `${event.clientY}px`;
|
||||||
|
contextMenu.style.left = `${event.clientX}px`;
|
||||||
|
|
||||||
|
// 显示右键菜单
|
||||||
|
contextMenu.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定右键点击事件到文件/文件夹
|
||||||
|
function bindRightClickMenu(treeData) {
|
||||||
|
treeData.forEach(item => {
|
||||||
|
item.div.oncontextmenu = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation();
|
||||||
|
selectItem(item);
|
||||||
|
showContextMenu(event, item);
|
||||||
|
};
|
||||||
|
if (item.children) {
|
||||||
|
bindRightClickMenu(item.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidName(name) {
|
||||||
|
// 检查是否为空或仅包含空格
|
||||||
|
if (!name.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含非法字符
|
||||||
|
const invalidChars = /[\/\\*?<>|":]/;
|
||||||
|
if (invalidChars.test(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件名是否过长(例如,限制为 255 字符)
|
||||||
|
if (name.length > 255) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否使用保留的文件名(Windows 系统常见保留名称)
|
||||||
|
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
|
||||||
|
if (reservedNames.includes(name.toUpperCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不允许以空格结尾
|
||||||
|
if (name.endsWith(' ')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 右键菜单项点击事件
|
||||||
|
document.getElementById("renameOption").onclick = () => {
|
||||||
|
if (selectedItem && selectedItem.type !== 'directory') {
|
||||||
|
const newName = prompt("Enter new name for the file:", selectedItem.name);
|
||||||
|
if (newName && checkValidName(newName)) {
|
||||||
|
fetch(`/vsc-like/rename-file`, { method: 'POST', body: JSON.stringify({ path: selectedItem.path+'/'+selectedItem.name, newName: newName }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => {
|
||||||
|
selectedItem.name = newName;
|
||||||
|
selectedItem.div.textContent = newName;
|
||||||
|
loadFileTree();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedItem && selectedItem.type === 'directory') {
|
||||||
|
const newName = prompt("Enter new name for the folder:", selectedItem.name);
|
||||||
|
if (newName && checkValidName(newName)) {
|
||||||
|
fetch(`/vsc-like/rename-folder`, { method: 'POST', body: JSON.stringify({ path: selectedItem.path+'/'+selectedItem.name, newName: newName }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => {
|
||||||
|
selectedItem.name = newName;
|
||||||
|
selectedItem.div.textContent = newName;
|
||||||
|
loadFileTree();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hideContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("copyOption").onclick = () => {
|
||||||
|
if (selectedItem) {
|
||||||
|
copiedItem = selectedItem;
|
||||||
|
console.log("Item copied:", copiedItem);
|
||||||
|
}
|
||||||
|
hideContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("pasteOption").onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (copiedItem) {
|
||||||
|
if (selectedItem.type === 'directory') {
|
||||||
|
newPath = selectedItem.path+'/'+selectedItem.name+'/';
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
newPath = selectedItem.path;
|
||||||
|
}
|
||||||
|
fetch(`/vsc-like/copy-files`, { method: 'POST', body: JSON.stringify({ oldPath: copiedItem.path+'/'+copiedItem.name, newPath: newPath }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => loadFileTree());
|
||||||
|
console.log("Item pasted:", selectedItem);
|
||||||
|
}
|
||||||
|
hideContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("deleteOption").onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
//二次确认
|
||||||
|
if (confirm("Are you sure you want to delete selected file/folder?")) {
|
||||||
|
if (selectedItem) {
|
||||||
|
fetch(`/vsc-like/delete-file`, { method: 'POST', body: JSON.stringify({ path: selectedItem.path+'/'+selectedItem.name }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => loadFileTree());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hideContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏右键菜单的点击事件
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const contextMenu = document.getElementById("contextMenu");
|
||||||
|
if (!contextMenu.contains(event.target)) {
|
||||||
|
hideContextMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function renderFileTree(treeData, container, level = 0, parent = null) {
|
||||||
|
treeData.forEach(item => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = item.name;
|
||||||
|
if (parent) {
|
||||||
|
item.path = parent + '/';
|
||||||
|
} else {
|
||||||
|
item.path = '.'
|
||||||
|
}
|
||||||
|
// 根据类型添加不同的类
|
||||||
|
if (item.type === 'directory') {
|
||||||
|
div.classList.add('tree-item', 'directory', `tree-item-level-${level}`); // 文件夹样式,添加层级类
|
||||||
|
const iconDiv = document.createElement('i');
|
||||||
|
div.prepend(iconDiv); // 为文件夹添加图标
|
||||||
|
|
||||||
|
const childrenDiv = document.createElement('div');
|
||||||
|
renderFileTree(item.children, childrenDiv, level + 1, item.path + '/' + item.name); // 递归调用,层级加1
|
||||||
|
div.appendChild(childrenDiv);
|
||||||
|
item.div = div;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
div.classList.add('tree-item', 'file', `tree-item-level-${level}`); // 文件样式,添加层级类
|
||||||
|
const iconDiv = document.createElement('i');
|
||||||
|
div.prepend(iconDiv); // 为文件添加图标
|
||||||
|
item.div = div;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (selectedItem) {
|
||||||
|
selectedItem.div.classList.remove('selected'); // 取消之前选中的文件夹/文件的选中状态
|
||||||
|
}
|
||||||
|
div.classList.add('selected'); // 为当前点击的文件夹/文件添加选中样式
|
||||||
|
selectedItem = item; // 更新选中项
|
||||||
|
hideContextMenu();
|
||||||
|
// 获取选中的文件或文件夹的详细信息
|
||||||
|
console.log("Selected Item:", item);
|
||||||
|
|
||||||
|
if (item.type === 'directory') {
|
||||||
|
showOrHideChildren(div);
|
||||||
|
} else {
|
||||||
|
loadFileContent(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function selectEmpty(div) {
|
||||||
|
if (selectedItem) {
|
||||||
|
selectedItem.div.classList.remove('selected');
|
||||||
|
}
|
||||||
|
selectedItem = {type: 'directory', path: '.', name: '.', div:div};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOrHideChildren(d) {
|
||||||
|
const childrenDiv = d.querySelector('div');
|
||||||
|
if (childrenDiv) {
|
||||||
|
// 切换显示和隐藏
|
||||||
|
childrenDiv.style.display = (childrenDiv.style.display === 'none' || childrenDiv.style.display === '') ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 加载文件内容
|
||||||
|
function loadFileContent(item) {
|
||||||
|
fetch(`/vsc-like/file-content`, { method: 'POST', body: JSON.stringify({ path: item.path+'/'+item.name }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.content) {
|
||||||
|
editor.setValue(data.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 新建文件
|
||||||
|
function createNewFile(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const fileName = prompt('Enter new file name:');
|
||||||
|
if (fileName) {
|
||||||
|
if (selectedItem) {
|
||||||
|
if (selectedItem.type === 'directory') {
|
||||||
|
fetch(`/vsc-like/create-file`, { method: 'POST', body: JSON.stringify({ path: fileName, parent: selectedItem.path+'/'+selectedItem.name }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => loadFileTree());
|
||||||
|
} else {
|
||||||
|
console.log("selectedItem.path",selectedItem.path);
|
||||||
|
fetch(`/vsc-like/create-file`, { method: 'POST', body: JSON.stringify({ path: fileName, parent: selectedItem.path }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => loadFileTree());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
function createNewFolder(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const folderName = prompt('Enter new folder name:');
|
||||||
|
if (folderName) {
|
||||||
|
if (selectedItem) {
|
||||||
|
if (selectedItem.type === 'directory') {
|
||||||
|
fetch(`/vsc-like/create-folder`, { method: 'POST', body: JSON.stringify({ path: fileName, parent: selectedItem.path+'/'+selectedItem.name }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => loadFileTree());
|
||||||
|
} else {
|
||||||
|
fetch(`/vsc-like/create-folder`, { method: 'POST', body: JSON.stringify({ path: fileName, parent: selectedItem.path }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => loadFileTree());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
function saveFile(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const content = editor.getValue();
|
||||||
|
fetch(`/vsc-like/save-file`, { method: 'POST', body: JSON.stringify({ path: item.path+'/'+item.name, content: content }) , headers: { 'Content-Type': 'application/json' } })
|
||||||
|
.then(() => loadFileTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function sortFileTree(treeData) {
|
||||||
|
// 排序函数
|
||||||
|
return treeData.sort((a, b) => {
|
||||||
|
// 首先按照 type 排序: 'directory' 优先
|
||||||
|
if (a.type === b.type) {
|
||||||
|
// 如果 type 相同,则按 name 排序
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
// 'directory' 在前,'file' 在后
|
||||||
|
return a.type === 'directory' ? -1 : 1;
|
||||||
|
}).map(item => {
|
||||||
|
// 对每个子元素(children)也进行排序
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
item.children = sortFileTree(item.children); // 递归排序子树
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
140
code-server-like/templates/index.html
Normal file
140
code-server-like/templates/index.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Cloud IDE</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { display: flex; height: 100vh; margin: 0; }
|
||||||
|
#fileTree { width: 250px; border-right: 1px solid #ddd; padding: 10px; overflow-y: auto; }
|
||||||
|
#editorContainer { flex-grow: 1; }
|
||||||
|
#terminal { height: 200px; border-top: 1px solid #ddd; }
|
||||||
|
#searchSidebar { border-left: 1px solid #ddd; display: none; overflow-y: auto; }
|
||||||
|
#topbar { padding: 5px; border-bottom: 1px solid #ddd; background-color: #f1f1f1; }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="/vsc-like/static/css/index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="fileTree" onclick="selectEmpty(this)">
|
||||||
|
<div id="topbar">
|
||||||
|
<button id="newFileBtn"><i class="fas fa-file"></i></button>
|
||||||
|
<button id="newFolderBtn"><i class="fas fa-folder"></i></button>
|
||||||
|
<button id="toggleSearchBtn"><i class="fas fa-search"></i></button>
|
||||||
|
<button id="refreshBtn"><i class="fas fa-sync-alt"></i></button>
|
||||||
|
<button id="saveBtn"><i class="fas fa-save"></i></button>
|
||||||
|
<button id="openTerminalBtn"><i class="fas fa-terminal"></i></button>
|
||||||
|
</div>
|
||||||
|
<div id="fileTreeContent"></div>
|
||||||
|
<div id="searchSidebar">
|
||||||
|
<h3>Global Search</h3>
|
||||||
|
<input type="text" id="searchInput" placeholder="Search..." oninput="searchFiles()">
|
||||||
|
<ul id="searchResults"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<div id="contextMenu" class="context-menu" style="display: none;">
|
||||||
|
<ul>
|
||||||
|
<li id="renameOption">Rename</li>
|
||||||
|
<li id="copyOption">Copy</li>
|
||||||
|
<li id="pasteOption">Paste</li>
|
||||||
|
<li id="deleteOption">Delete</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editorContainer"></div>
|
||||||
|
<div id="terminal"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.js"></script>
|
||||||
|
<script src="/vsc-like/static/js/file.js"></script>
|
||||||
|
<script>
|
||||||
|
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } });
|
||||||
|
|
||||||
|
require(['vs/editor/editor.main'], function() {
|
||||||
|
editor = monaco.editor.create(document.getElementById('editorContainer'), {
|
||||||
|
value: '',
|
||||||
|
language: 'javascript'
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileTreeDiv = document.getElementById('fileTreeContent');
|
||||||
|
const searchSidebar = document.getElementById('searchSidebar');
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const searchResults = document.getElementById('searchResults');
|
||||||
|
const saveBtn = document.getElementById('saveBtn');
|
||||||
|
const openTerminalBtn = document.getElementById('openTerminalBtn');
|
||||||
|
|
||||||
|
|
||||||
|
// 切换全局搜索侧栏
|
||||||
|
function toggleSearchSidebar(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
searchSidebar.style.display = (searchSidebar.style.display === 'none' || searchSidebar.style.display === '') ? 'block' : 'none';
|
||||||
|
if (searchSidebar.style.display === 'block') {
|
||||||
|
fileTreeDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
fileTreeDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新文件树
|
||||||
|
function refreshFileTree(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
loadFileTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索文件
|
||||||
|
function searchFiles() {
|
||||||
|
const query = searchInput.value;
|
||||||
|
fetch(`/vsc-like/search-files?query=${query}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
searchResults.innerHTML = '';
|
||||||
|
data.forEach(result => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = result.name;
|
||||||
|
li.onclick = () => loadFileContent(result);
|
||||||
|
searchResults.appendChild(li);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let socket = null;
|
||||||
|
function openTerminal(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const terminalDiv = document.getElementById('terminal');
|
||||||
|
terminalDiv.style.display = (terminalDiv.style.display === 'none' || terminalDiv.style.display === '') ? 'block' : 'none';
|
||||||
|
if (socket == null) {
|
||||||
|
socket = io.connect('http://' + document.domain + ':5200/vsc-like/terminal');
|
||||||
|
}
|
||||||
|
socket.on('connected', function(data) {
|
||||||
|
console.log('Terminal connected');
|
||||||
|
});
|
||||||
|
socket.on('terminal_output', function(data) {
|
||||||
|
const terminalDiv = document.getElementById('terminal');
|
||||||
|
terminalDiv.textContent += data.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendTerminalInput(input) {
|
||||||
|
socket.emit('send_input', { input: input });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮绑定事件
|
||||||
|
document.getElementById('newFileBtn').addEventListener('click', (event) => createNewFile(event));
|
||||||
|
document.getElementById('newFolderBtn').addEventListener('click', (event) => createNewFolder(event));
|
||||||
|
document.getElementById('toggleSearchBtn').addEventListener('click', (event) => toggleSearchSidebar(event));
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', (event) => refreshFileTree(event));
|
||||||
|
document.getElementById('saveBtn').addEventListener('click', (event) => saveFile(event));
|
||||||
|
document.getElementById('openTerminalBtn').addEventListener('click', (event) => openTerminal(event));
|
||||||
|
|
||||||
|
loadFileTree();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
study/test/.config
Normal file
1
study/test/.config
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"uuid":"123456", "path":"test"}
|
||||||
0
study/test/123123/newinside
Normal file
0
study/test/123123/newinside
Normal file
0
study/test/123123/newnewnew
Normal file
0
study/test/123123/newnewnew
Normal file
0
study/test/123123/newnewnewnew
Normal file
0
study/test/123123/newnewnewnew
Normal file
0
study/test/newinside
Normal file
0
study/test/newinside
Normal file
0
study/test/newoutside
Normal file
0
study/test/newoutside
Normal file
Reference in New Issue
Block a user