|
|
<!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 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> |