diff --git a/code-server-like/app/__init__.py b/code-server-like/app/__init__.py new file mode 100644 index 0000000..93a36bc --- /dev/null +++ b/code-server-like/app/__init__.py @@ -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 diff --git a/code-server-like/app/config.py b/code-server-like/app/config.py new file mode 100644 index 0000000..a370521 --- /dev/null +++ b/code-server-like/app/config.py @@ -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'] + diff --git a/code-server-like/app/extensions.py b/code-server-like/app/extensions.py new file mode 100644 index 0000000..1d9b1c6 --- /dev/null +++ b/code-server-like/app/extensions.py @@ -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 \ No newline at end of file diff --git a/code-server-like/app/services/file_service.py b/code-server-like/app/services/file_service.py new file mode 100644 index 0000000..afdfc42 --- /dev/null +++ b/code-server-like/app/services/file_service.py @@ -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 \ No newline at end of file diff --git a/code-server-like/app/sockets/terminal_service.py b/code-server-like/app/sockets/terminal_service.py new file mode 100644 index 0000000..b6ab74f --- /dev/null +++ b/code-server-like/app/sockets/terminal_service.py @@ -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] diff --git a/code-server-like/app/views/file.py b/code-server-like/app/views/file.py new file mode 100644 index 0000000..d5f5051 --- /dev/null +++ b/code-server-like/app/views/file.py @@ -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) + + diff --git a/code-server-like/app/views/routes.py b/code-server-like/app/views/routes.py new file mode 100644 index 0000000..276185f --- /dev/null +++ b/code-server-like/app/views/routes.py @@ -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') + + diff --git a/code-server-like/config.ini b/code-server-like/config.ini new file mode 100644 index 0000000..31d1171 --- /dev/null +++ b/code-server-like/config.ini @@ -0,0 +1,3 @@ +[Global] +SECRET_KEY = cakebaker +ROOT_WORKSPACE_PATH = ../study diff --git a/code-server-like/requirements.txt b/code-server-like/requirements.txt new file mode 100644 index 0000000..c8de37e --- /dev/null +++ b/code-server-like/requirements.txt @@ -0,0 +1,3 @@ +Flask +Flask-SocketIO +python-dotenv diff --git a/code-server-like/run.py b/code-server-like/run.py new file mode 100644 index 0000000..1240f8f --- /dev/null +++ b/code-server-like/run.py @@ -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 的安全限制提示 + ) diff --git a/code-server-like/static/css/index.css b/code-server-like/static/css/index.css new file mode 100644 index 0000000..4411d97 --- /dev/null +++ b/code-server-like/static/css/index.css @@ -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; +} diff --git a/code-server-like/static/js/file.js b/code-server-like/static/js/file.js new file mode 100644 index 0000000..09dccc0 --- /dev/null +++ b/code-server-like/static/js/file.js @@ -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; + }); +} + + diff --git a/code-server-like/templates/index.html b/code-server-like/templates/index.html new file mode 100644 index 0000000..a0824fa --- /dev/null +++ b/code-server-like/templates/index.html @@ -0,0 +1,140 @@ + + +
+ + +