qwen_agent/public/file-manager.html
2025-11-08 20:23:04 +08:00

990 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件管理器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.toolbar {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.file-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.file-item {
display: flex;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.selected {
background-color: #e3f2fd;
}
.file-icon {
width: 24px;
height: 24px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 500;
margin-bottom: 2px;
}
.file-meta {
font-size: 12px;
color: #666;
}
.file-actions {
display: flex;
gap: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #1976d2;
color: white;
}
.btn-primary:hover {
background-color: #1565c0;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #138496;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 500px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 15px;
font-size: 14px;
}
.breadcrumb-item {
cursor: pointer;
color: #1976d2;
}
.breadcrumb-item:hover {
text-decoration: underline;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.upload-area:hover {
border-color: #1976d2;
}
.upload-area.dragover {
border-color: #1976d2;
background-color: #f0f8ff;
}
.search-box {
flex: 1;
min-width: 200px;
}
.search-box input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.search-box {
min-width: auto;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📁 项目文件管理器</h1>
<p>管理您的项目文件,支持上传、下载、删除等操作</p>
</div>
<div class="toolbar">
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索文件..." />
</div>
<button class="btn btn-primary" onclick="showUploadModal()">📤 上传文件</button>
<button class="btn btn-success" onclick="showCreateFolderModal()">📁 新建文件夹</button>
<button class="btn btn-secondary" onclick="refreshFileList()">🔄 刷新</button>
<button class="btn btn-info" onclick="toggleBatchMode()" id="batchModeBtn">📦 批量操作</button>
<button class="btn btn-primary" onclick="selectAll()" id="selectAllBtn" style="display: none;">☑️ 全选</button>
<button class="btn btn-primary" onclick="downloadSelected()" id="downloadSelectedBtn" style="display: none;">📥 下载选中</button>
</div>
<div class="breadcrumb" id="breadcrumb">
<span class="breadcrumb-item" onclick="navigateToPath('')">🏠 根目录</span>
</div>
<div class="file-list" id="fileList">
<!-- 文件列表将在这里动态生成 -->
</div>
</div>
<!-- 上传文件模态框 -->
<div id="uploadModal" class="modal">
<div class="modal-content">
<h3>上传文件</h3>
<div class="upload-area" id="uploadArea">
<p>📁 拖拽文件到这里或点击选择文件</p>
<input type="file" id="fileInput" multiple style="display: none;" />
</div>
<div style="margin-top: 15px; text-align: right;">
<button class="btn btn-secondary" onclick="closeModal('uploadModal')">取消</button>
</div>
</div>
</div>
<!-- 新建文件夹模态框 -->
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h3>新建文件夹</h3>
<div class="form-group">
<label>文件夹名称:</label>
<input type="text" id="folderNameInput" placeholder="输入文件夹名称" />
</div>
<div style="text-align: right;">
<button class="btn btn-secondary" onclick="closeModal('createFolderModal')">取消</button>
<button class="btn btn-primary" onclick="createFolder()">创建</button>
</div>
</div>
</div>
<!-- 重命名模态框 -->
<div id="renameModal" class="modal">
<div class="modal-content">
<h3>重命名</h3>
<div class="form-group">
<label>新名称:</label>
<input type="text" id="renameInput" placeholder="输入新名称" />
</div>
<div style="text-align: right;">
<button class="btn btn-secondary" onclick="closeModal('renameModal')">取消</button>
<button class="btn btn-primary" onclick="renameItem()">重命名</button>
</div>
</div>
</div>
<script>
const API_BASE = `${window.location.protocol}//${window.location.host}/api/v1/files`;
let currentPath = '';
let selectedItems = new Set();
let currentItemToRename = '';
let batchMode = false;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadFileList();
setupEventListeners();
});
function setupEventListeners() {
// 搜索功能
document.getElementById('searchInput').addEventListener('input', function(e) {
if (e.target.value.trim()) {
searchFiles(e.target.value.trim());
} else {
loadFileList();
}
});
// 文件上传
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
// 拖拽上传
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
// 模态框点击外部关闭
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
}
});
});
}
async function loadFileList(path = '') {
try {
const response = await fetch(`${API_BASE}/list?path=${encodeURIComponent(path)}&recursive=false`);
const data = await response.json();
if (data.success) {
currentPath = path;
renderFileList(data.items);
updateBreadcrumb(path);
}
} catch (error) {
console.error('加载文件列表失败:', error);
showMessage('加载文件列表失败', 'error');
}
}
function renderFileList(items) {
const fileList = document.getElementById('fileList');
fileList.innerHTML = '';
// 返回上级目录
if (currentPath) {
const parentItem = createFileItem({
name: '..',
type: 'directory',
path: getParentPath(currentPath),
modified: Date.now() / 1000
});
fileList.appendChild(parentItem);
}
// 渲染文件和文件夹
items.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'directory' ? -1 : 1;
}
return a.name.localeCompare(b.name);
}).forEach(item => {
const fileItem = createFileItem(item);
fileList.appendChild(fileItem);
});
}
function createFileItem(item) {
const div = document.createElement('div');
div.className = 'file-item';
div.dataset.path = item.path;
const icon = item.type === 'directory' ? '📁' : getFileIcon(item.name);
const size = item.type === 'file' ? formatFileSize(item.size) : '-';
const date = new Date(item.modified * 1000).toLocaleDateString();
const isSelected = selectedItems.has(item.path);
// 构建复选框HTML
const checkboxHtml = batchMode ?
`<input type="checkbox" ${isSelected ? 'checked' : ''} onchange="toggleItemSelection('${item.path}')" style="margin-right: 12px;">` : '';
div.innerHTML = `
${checkboxHtml}
<div class="file-icon">${icon}</div>
<div class="file-info">
<div class="file-name">${item.name}</div>
<div class="file-meta">${size}${date}</div>
</div>
<div class="file-actions">
${item.type === 'file' ?
`<button class="btn btn-primary" onclick="downloadFile('${item.path}')">下载</button>` :
`<button class="btn btn-primary" onclick="downloadFolderAsZip('${item.path}')">下载ZIP</button>`
}
<button class="btn btn-secondary" onclick="showRenameModal('${item.path}')">重命名</button>
<button class="btn btn-danger" onclick="deleteItem('${item.path}')">删除</button>
</div>
`;
// 添加批量模式下的点击事件
div.addEventListener('click', (e) => {
if (e.target.closest('.file-actions') || e.target.type === 'checkbox') return;
if (batchMode) {
toggleItemSelection(item.path);
// 更新复选框状态
const checkbox = div.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = selectedItems.has(item.path);
}
} else {
if (item.type === 'directory') {
loadFileList(item.path);
} else {
window.open(`${API_BASE}/download/${item.path}`, '_blank');
}
}
});
// 添加选中状态的样式
if (isSelected) {
div.classList.add('selected');
}
return div;
}
function getFileIcon(fileName) {
const ext = fileName.split('.').pop().toLowerCase();
const iconMap = {
'md': '📄',
'txt': '📝',
'pdf': '📕',
'doc': '📘',
'docx': '📘',
'xls': '📗',
'xlsx': '📗',
'ppt': '📙',
'pptx': '📙',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️',
'zip': '📦',
'rar': '📦',
'mp4': '🎬',
'mp3': '🎵',
'json': '📋',
'xml': '📋',
'html': '🌐',
'css': '🎨',
'js': '⚡',
'py': '🐍'
};
return iconMap[ext] || '📄';
}
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(1)) + ' ' + sizes[i];
}
function updateBreadcrumb(path) {
const breadcrumb = document.getElementById('breadcrumb');
breadcrumb.innerHTML = '<span class="breadcrumb-item" onclick="navigateToPath(\'\')">🏠 根目录</span>';
if (path) {
const parts = path.split('/');
let currentPath = '';
parts.forEach((part, index) => {
currentPath += (currentPath ? '/' : '') + part;
const isLast = index === parts.length - 1;
if (!isLast) {
breadcrumb.innerHTML += ` <span></span> <span class="breadcrumb-item" onclick="navigateToPath('${currentPath}')">${part}</span>`;
} else {
breadcrumb.innerHTML += ` <span></span> <span>${part}</span>`;
}
});
}
}
function navigateToPath(path) {
loadFileList(path);
}
function getParentPath(path) {
const parts = path.split('/');
return parts.slice(0, -1).join('/');
}
async function searchFiles(query) {
try {
const response = await fetch(`${API_BASE}/search?query=${encodeURIComponent(query)}&path=${encodeURIComponent(currentPath)}`);
const data = await response.json();
if (data.success) {
renderFileList(data.results.map(item => ({
...item,
modified: Date.now() / 1000,
size: 0
})));
}
} catch (error) {
console.error('搜索失败:', error);
showMessage('搜索失败', 'error');
}
}
function handleFileSelect(e) {
handleFiles(e.target.files);
}
async function handleFiles(files) {
for (let file of files) {
await uploadFile(file);
}
closeModal('uploadModal');
loadFileList();
}
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('path', currentPath);
try {
const response = await fetch(`${API_BASE}/upload`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showMessage(`文件 ${file.name} 上传成功`, 'success');
} else {
showMessage(`文件 ${file.name} 上传失败`, 'error');
}
} catch (error) {
console.error('上传失败:', error);
showMessage(`文件 ${file.name} 上传失败`, 'error');
}
}
async function deleteItem(path) {
if (!confirm(`确定要删除 ${path.split('/').pop()} 吗?`)) return;
try {
const response = await fetch(`${API_BASE}/delete?path=${encodeURIComponent(path)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showMessage('删除成功', 'success');
loadFileList();
} else {
showMessage('删除失败', 'error');
}
} catch (error) {
console.error('删除失败:', error);
showMessage('删除失败', 'error');
}
}
async function createFolder() {
const folderName = document.getElementById('folderNameInput').value.trim();
if (!folderName) {
showMessage('请输入文件夹名称', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/create-folder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: currentPath,
name: folderName
})
});
const data = await response.json();
if (data.success) {
showMessage('文件夹创建成功', 'success');
closeModal('createFolderModal');
loadFileList();
} else {
showMessage('文件夹创建失败', 'error');
}
} catch (error) {
console.error('创建文件夹失败:', error);
showMessage('创建文件夹失败', 'error');
}
}
async function renameItem() {
const newName = document.getElementById('renameInput').value.trim();
if (!newName) {
showMessage('请输入新名称', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/rename`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
old_path: currentItemToRename,
new_name: newName
})
});
const data = await response.json();
if (data.success) {
showMessage('重命名成功', 'success');
closeModal('renameModal');
loadFileList();
} else {
showMessage('重命名失败', 'error');
}
} catch (error) {
console.error('重命名失败:', error);
showMessage('重命名失败', 'error');
}
}
function downloadFile(path) {
window.open(`${API_BASE}/download/${path}`, '_blank');
}
async function downloadFolderAsZip(folderPath) {
const folderName = folderPath.split('/').pop();
const downloadButton = event.target;
// 禁用按钮,显示加载状态
const originalText = downloadButton.textContent;
downloadButton.disabled = true;
downloadButton.textContent = '压缩中...';
try {
// 显示压缩开始提示
showMessage(`正在压缩文件夹 "${folderName}"...`, 'info');
// 使用新的下载文件夹ZIP接口
const response = await fetch(`${API_BASE}/download-folder-zip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ path: folderPath })
});
if (response.ok) {
// 获取文件名
const contentDisposition = response.headers.get('content-disposition');
let fileName = `${folderName}.zip`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
if (fileNameMatch) {
fileName = fileNameMatch[1];
}
}
// 检查文件大小
const contentLength = response.headers.get('content-length');
const fileSize = contentLength ? parseInt(contentLength) : 0;
if (fileSize > 100 * 1024 * 1024) { // 大于100MB显示警告
showMessage(`文件较大 (${formatFileSize(fileSize)}),下载可能需要一些时间...`, 'warning');
}
downloadButton.textContent = '下载中...';
// 创建下载链接
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showMessage(`文件夹 "${folderName}" 压缩下载成功`, 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `服务器错误: ${response.status}`);
}
} catch (error) {
console.error('下载文件夹失败:', error);
showMessage(`下载文件夹 "${folderName}" 失败: ${error.message}`, 'error');
} finally {
// 恢复按钮状态
downloadButton.disabled = false;
downloadButton.textContent = originalText;
}
}
function refreshFileList() {
loadFileList(currentPath);
}
function toggleBatchMode() {
batchMode = !batchMode;
const batchBtn = document.getElementById('batchModeBtn');
const downloadBtn = document.getElementById('downloadSelectedBtn');
const selectAllBtn = document.getElementById('selectAllBtn');
if (batchMode) {
batchBtn.textContent = '❌ 退出批量';
batchBtn.classList.remove('btn-info');
batchBtn.classList.add('btn-secondary');
downloadBtn.style.display = 'inline-block';
selectAllBtn.style.display = 'inline-block';
selectedItems.clear();
} else {
batchBtn.textContent = '📦 批量操作';
batchBtn.classList.remove('btn-secondary');
batchBtn.classList.add('btn-info');
downloadBtn.style.display = 'none';
selectAllBtn.style.display = 'none';
selectedItems.clear();
}
refreshFileList(); // 重新渲染以显示/隐藏复选框
}
function selectAll() {
const allItems = document.querySelectorAll('.file-item[data-path]');
const allPaths = Array.from(allItems).map(item => item.dataset.path);
if (selectedItems.size === allPaths.length) {
// 如果已全选,则取消全选
selectedItems.clear();
document.getElementById('selectAllBtn').textContent = '☑️ 全选';
} else {
// 全选
selectedItems.clear();
allPaths.forEach(path => selectedItems.add(path));
document.getElementById('selectAllBtn').textContent = '❌ 取消全选';
}
// 更新所有复选框和选中状态
allItems.forEach(item => {
const path = item.dataset.path;
const checkbox = item.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = selectedItems.has(path);
}
if (selectedItems.has(path)) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
updateBatchUI();
}
function toggleItemSelection(path) {
if (selectedItems.has(path)) {
selectedItems.delete(path);
} else {
selectedItems.add(path);
}
updateBatchUI();
}
function updateBatchUI() {
const downloadBtn = document.getElementById('downloadSelectedBtn');
const count = selectedItems.size;
if (count > 0) {
downloadBtn.textContent = `📥 下载选中 (${count})`;
downloadBtn.classList.remove('btn-primary');
downloadBtn.classList.add('btn-success');
} else {
downloadBtn.textContent = '📥 下载选中';
downloadBtn.classList.remove('btn-success');
downloadBtn.classList.add('btn-primary');
}
}
async function downloadSelected() {
if (selectedItems.size === 0) {
showMessage('请先选择要下载的文件', 'warning');
return;
}
const items = Array.from(selectedItems);
const hasFolders = items.some(path => {
const fullPath = PROJECTS_DIR / path;
return fullPath.isDirectory();
});
if (hasFolders) {
showMessage('检测到文件夹,正在创建压缩包...', 'info');
await downloadItemsAsZip(items);
} else {
// 下载单个文件
for (const item of items) {
downloadFile(item);
}
}
}
async function downloadItemsAsZip(items) {
try {
showMessage(`正在压缩 ${items.length} 个项目...`, 'info');
const response = await fetch(`${API_BASE}/download-multiple-zip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paths: items,
filename: `batch_download_${new Date().getTime()}.zip`
})
});
if (response.ok) {
const contentDisposition = response.headers.get('content-disposition');
let fileName = `batch_download.zip`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
if (fileNameMatch) {
fileName = fileNameMatch[1];
}
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showMessage('批量下载成功', 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `服务器错误: ${response.status}`);
}
} catch (error) {
console.error('批量下载失败:', error);
showMessage(`批量下载失败: ${error.message}`, 'error');
}
}
function showUploadModal() {
document.getElementById('uploadModal').style.display = 'block';
document.getElementById('fileInput').value = '';
}
function showCreateFolderModal() {
document.getElementById('createFolderModal').style.display = 'block';
document.getElementById('folderNameInput').value = '';
}
function showRenameModal(path) {
currentItemToRename = path;
const fileName = path.split('/').pop();
document.getElementById('renameInput').value = fileName;
document.getElementById('renameModal').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
function showMessage(message, type) {
// 消息提示,支持多种类型
const toast = document.createElement('div');
let bgColor = '#6c757d'; // 默认灰色
switch(type) {
case 'success':
bgColor = '#28a745';
break;
case 'error':
bgColor = '#dc3545';
break;
case 'info':
bgColor = '#17a2b8';
break;
case 'warning':
bgColor = '#ffc107';
break;
}
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: ${type === 'warning' ? '#212529' : 'white'};
z-index: 10000;
background-color: ${bgColor};
max-width: 300px;
word-wrap: break-word;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
`;
toast.textContent = message;
document.body.appendChild(toast);
// 自动移除
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 4000);
}
</script>
</body>
</html>