code-server-like
This commit is contained in:
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user