webui / templates /files.html
BG5's picture
Upload 7 files
a4d706b verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件管理 - WebShell</title>
<link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/theme/monokai.min.css" rel="stylesheet">
<style>
body {
background: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,.1);
}
.file-item {
cursor: pointer;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.selected {
background-color: #e3f2fd;
}
.breadcrumb {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.1);
}
.file-icon {
width: 20px;
text-align: center;
}
.CodeMirror {
border: 1px solid #ddd;
border-radius: 4px;
height: 400px;
}
.toolbar {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.1);
padding: 1rem;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="fas fa-folder-open me-2"></i>文件管理
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/terminal">
<i class="fas fa-terminal me-2"></i>终端
</a>
<a class="nav-link" href="/">
<i class="fas fa-home me-2"></i>主页
</a>
</div>
</div>
</nav>
<div class="container-fluid py-3">
<div class="row">
<!-- 文件列表 -->
<div class="col-md-8">
<!-- 路径导航 -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb p-3 mb-0" id="breadcrumb">
<li class="breadcrumb-item"><a href="#" onclick="loadFiles('.')">根目录</a></li>
</ol>
</nav>
<!-- 工具栏 -->
<div class="toolbar d-flex justify-content-between align-items-center">
<div>
<button class="btn btn-primary btn-sm" onclick="showCreateDialog()">
<i class="fas fa-plus me-2"></i>新建
</button>
<button class="btn btn-success btn-sm" onclick="document.getElementById('uploadFile').click()">
<i class="fas fa-upload me-2"></i>上传
</button>
<button class="btn btn-warning btn-sm" onclick="renameSelected()" id="renameBtn" disabled>
<i class="fas fa-edit me-2"></i>重命名
</button>
<button class="btn btn-danger btn-sm" onclick="deleteSelected()" id="deleteBtn" disabled>
<i class="fas fa-trash me-2"></i>删除
</button>
</div>
<div>
<button class="btn btn-outline-secondary btn-sm" onclick="refreshFiles()">
<i class="fas fa-sync me-2"></i>刷新
</button>
</div>
</div>
<!-- 文件列表 -->
<div class="card">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="40"></th>
<th>名称</th>
<th width="100">大小</th>
<th width="150">修改时间</th>
<th width="100">操作</th>
</tr>
</thead>
<tbody id="fileList">
<!-- 文件列表将在这里动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 预览/编辑面板 -->
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h6 class="mb-0" id="previewTitle">选择文件进行预览</h6>
</div>
<div class="card-body" id="previewContent">
<div class="text-center text-muted">
<i class="fas fa-file fa-3x mb-3"></i>
<p>选择一个文件来预览或编辑其内容</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 上传文件隐藏input -->
<input type="file" id="uploadFile" style="display: none" multiple onchange="uploadFiles(this.files)">
<!-- 新建文件/文件夹模态框 -->
<div class="modal fade" id="createModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">新建</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">类型</label>
<select class="form-select" id="createType">
<option value="file">文件</option>
<option value="directory">文件夹</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">名称</label>
<input type="text" class="form-control" id="createName" placeholder="输入名称">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="createItem()">创建</button>
</div>
</div>
</div>
</div>
<!-- 编辑器模态框 -->
<div class="modal fade" id="editorModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editorTitle">编辑文件</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<textarea id="fileEditor"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveFile()">保存</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/python/python.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/xml/xml.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/css/css.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/htmlmixed/htmlmixed.min.js"></script>
<script>
let currentPath = '.';
let selectedFile = null;
let editor = null;
let currentEditFile = null;
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadFiles('.');
initEditor();
});
// 初始化编辑器
function initEditor() {
editor = CodeMirror.fromTextArea(document.getElementById('fileEditor'), {
theme: 'monokai',
lineNumbers: true,
mode: 'text/plain',
indentUnit: 4,
lineWrapping: true
});
}
// 加载文件列表
function loadFiles(path) {
currentPath = path;
fetch(`/api/files?path=${encodeURIComponent(path)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderFileList(data.items);
updateBreadcrumb(data.path);
} else {
alert('加载文件失败: ' + data.error);
}
})
.catch(error => {
alert('网络错误: ' + error.message);
});
}
// 渲染文件列表
function renderFileList(items) {
const fileList = document.getElementById('fileList');
fileList.innerHTML = '';
// 添加上级目录
if (currentPath !== '.') {
const row = document.createElement('tr');
row.className = 'file-item';
row.innerHTML = `
<td><i class="fas fa-level-up-alt file-icon"></i></td>
<td><a href="#" onclick="loadFiles('${getParentPath(currentPath)}')">..</a></td>
<td>-</td>
<td>-</td>
<td>-</td>
`;
fileList.appendChild(row);
}
// 添加文件和文件夹
items.forEach(item => {
const row = document.createElement('tr');
row.className = 'file-item';
row.onclick = () => selectFile(row, item);
row.ondblclick = () => {
if (item.type === 'directory') {
loadFiles(item.path);
} else {
editFile(item.path);
}
};
const icon = item.type === 'directory' ? 'fa-folder' : getFileIcon(item.name);
const size = item.type === 'directory' ? '-' : formatFileSize(item.size);
row.innerHTML = `
<td><i class="fas ${icon} file-icon"></i></td>
<td>${item.name}</td>
<td>${size}</td>
<td>${item.modified}</td>
<td>
${item.type === 'file' ? `
<button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); editFile('${item.path}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-success" onclick="event.stopPropagation(); downloadFile('${item.path}')">
<i class="fas fa-download"></i>
</button>
` : ''}
</td>
`;
fileList.appendChild(row);
});
}
// 选择文件
function selectFile(row, item) {
// 移除之前的选中状态
document.querySelectorAll('.file-item').forEach(r => r.classList.remove('selected'));
row.classList.add('selected');
selectedFile = item;
updateButtons();
if (item.type === 'file') {
previewFile(item.path);
}
}
// 更新按钮状态
function updateButtons() {
const renameBtn = document.getElementById('renameBtn');
const deleteBtn = document.getElementById('deleteBtn');
if (selectedFile) {
renameBtn.disabled = false;
deleteBtn.disabled = false;
} else {
renameBtn.disabled = true;
deleteBtn.disabled = true;
}
}
// 预览文件
function previewFile(filePath) {
const previewTitle = document.getElementById('previewTitle');
const previewContent = document.getElementById('previewContent');
previewTitle.textContent = `预览: ${filePath.split('/').pop()}`;
fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const content = data.content;
if (content.length > 1000) {
previewContent.innerHTML = `
<div class="alert alert-info">
<p><strong>文件太大,只显示前1000个字符</strong></p>
<pre style="max-height: 300px; overflow-y: auto;">${escapeHtml(content.substring(0, 1000))}...</pre>
<button class="btn btn-primary btn-sm mt-2" onclick="editFile('${filePath}')">
<i class="fas fa-edit me-2"></i>编辑完整文件
</button>
</div>
`;
} else {
previewContent.innerHTML = `
<pre style="max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 1rem; border-radius: 4px;">${escapeHtml(content)}</pre>
<button class="btn btn-primary btn-sm mt-2" onclick="editFile('${filePath}')">
<i class="fas fa-edit me-2"></i>编辑文件
</button>
`;
}
} else {
previewContent.innerHTML = `
<div class="alert alert-warning">
无法预览此文件: ${data.error}
</div>
`;
}
})
.catch(error => {
previewContent.innerHTML = `
<div class="alert alert-danger">
预览失败: ${error.message}
</div>
`;
});
}
// 编辑文件
function editFile(filePath) {
currentEditFile = filePath;
fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
editor.setValue(data.content);
editor.setOption('mode', getEditorMode(filePath));
document.getElementById('editorTitle').textContent = `编辑: ${filePath.split('/').pop()}`;
new bootstrap.Modal(document.getElementById('editorModal')).show();
} else {
alert('读取文件失败: ' + data.error);
}
})
.catch(error => {
alert('网络错误: ' + error.message);
});
}
// 保存文件
function saveFile() {
if (!currentEditFile) return;
const content = editor.getValue();
fetch('/api/files/write', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: currentEditFile,
content: content
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('editorModal')).hide();
refreshFiles();
} else {
alert('保存失败: ' + data.error);
}
})
.catch(error => {
alert('网络错误: ' + error.message);
});
}
// 显示创建对话框
function showCreateDialog() {
new bootstrap.Modal(document.getElementById('createModal')).show();
}
// 创建文件或文件夹
function createItem() {
const type = document.getElementById('createType').value;
const name = document.getElementById('createName').value.trim();
if (!name) {
alert('请输入名称');
return;
}
const path = currentPath === '.' ? name : `${currentPath}/${name}`;
fetch('/api/files/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: path,
type: type
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('createModal')).hide();
document.getElementById('createName').value = '';
refreshFiles();
} else {
alert('创建失败: ' + data.error);
}
})
.catch(error => {
alert('网络错误: ' + error.message);
});
}
// 上传文件
function uploadFiles(files) {
Array.from(files).forEach(file => {
const formData = new FormData();
formData.append('file', file);
formData.append('path', currentPath);
fetch('/api/files/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
refreshFiles();
} else {
alert(`上传 ${file.name} 失败: ${data.error}`);
}
})
.catch(error => {
alert(`上传 ${file.name} 网络错误: ${error.message}`);
});
});
}
// 重命名选中文件
function renameSelected() {
if (!selectedFile) return;
const newName = prompt('请输入新名称:', selectedFile.name);
if (!newName || newName === selectedFile.name) return;
const newPath = selectedFile.path.replace(/[^/\\]*$/, newName);
fetch('/api/files/rename', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
oldPath: selectedFile.path,
newPath: newPath
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
refreshFiles();
selectedFile = null;
updateButtons();
} else {
alert('重命名失败: ' + data.error);
}
})
.catch(error => {
alert('网络错误: ' + error.message);
});
}
// 删除选中文件
function deleteSelected() {
if (!selectedFile) return;
if (!confirm(`确定要删除 "${selectedFile.name}" 吗?`)) return;
fetch('/api/files/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: selectedFile.path
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
refreshFiles();
selectedFile = null;
updateButtons();
} else {
alert('删除失败: ' + data.error);
}
})
.catch(error => {
alert('网络错误: ' + error.message);
});
}
// 下载文件
function downloadFile(filePath) {
window.open(`/api/files/download?path=${encodeURIComponent(filePath)}`);
}
// 刷新文件列表
function refreshFiles() {
loadFiles(currentPath);
}
// 工具函数
function getParentPath(path) {
const parts = path.split('/').filter(p => p && p !== '.');
parts.pop();
return parts.length > 0 ? parts.join('/') : '.';
}
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const iconMap = {
'txt': 'fa-file-alt',
'js': 'fa-file-code',
'html': 'fa-file-code',
'css': 'fa-file-code',
'py': 'fa-file-code',
'jpg': 'fa-file-image',
'png': 'fa-file-image',
'gif': 'fa-file-image',
'pdf': 'fa-file-pdf',
'zip': 'fa-file-archive',
'rar': 'fa-file-archive'
};
return iconMap[ext] || 'fa-file';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getEditorMode(filename) {
const ext = filename.split('.').pop().toLowerCase();
const modeMap = {
'js': 'javascript',
'html': 'htmlmixed',
'css': 'css',
'py': 'python',
'xml': 'xml',
'json': 'javascript'
};
return modeMap[ext] || 'text/plain';
}
function updateBreadcrumb(path) {
const breadcrumb = document.getElementById('breadcrumb');
breadcrumb.innerHTML = '<li class="breadcrumb-item"><a href="#" onclick="loadFiles(\'.\')">根目录</a></li>';
if (path !== '.' && path !== '') {
const parts = path.split('/').filter(p => p);
let currentBreadcrumbPath = '';
parts.forEach((part, index) => {
currentBreadcrumbPath += (currentBreadcrumbPath ? '/' : '') + part;
const isLast = index === parts.length - 1;
if (isLast) {
breadcrumb.innerHTML += `<li class="breadcrumb-item active">${part}</li>`;
} else {
breadcrumb.innerHTML += `<li class="breadcrumb-item"><a href="#" onclick="loadFiles('${currentBreadcrumbPath}')">${part}</a></li>`;
}
});
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>