qwen_agent/public/admin.html
2025-11-14 23:35:12 +08:00

2228 lines
89 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>管理后台 - Qwen Agent</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.8/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.13.1/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--success-color: #198754;
--warning-color: #ffc107;
--error-color: #dc3545;
--info-color: #0dcaf0;
--sidebar-bg: #212529;
--sidebar-text: #ffffff;
--card-bg: #ffffff;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f8f9fa;
color: var(--text-primary);
line-height: 1.6;
height: 100vh;
overflow: hidden;
}
/* 登录页面样式 */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
background: white;
padding: 2.5rem;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: var(--primary-color);
font-size: 2rem;
margin-bottom: 0.5rem;
}
/* 管理后台主界面样式 */
.admin-container {
display: none; /* 默认隐藏,登录后显示 */
height: 100vh;
overflow: hidden;
}
.sidebar {
width: 280px;
background-color: var(--sidebar-bg);
color: var(--sidebar-text);
height: 100vh;
overflow-y: auto;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header {
padding: 1.5rem;
background-color: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.sidebar-nav {
padding: 0;
}
.nav-item {
display: block;
padding: 0.75rem 1.5rem;
color: var(--sidebar-text);
text-decoration: none;
transition: background-color 0.3s;
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
font-size: 0.95rem;
border-radius: 0;
}
.nav-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.nav-item.active {
background-color: var(--primary-color);
border-left: 4px solid #64b5f6;
color: white;
}
.main-content {
height: 100vh;
overflow-y: auto;
background-color: #f8f9fa;
}
.content-header {
background-color: white;
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 10;
}
.content-header h1 {
font-size: 1.75rem;
color: var(--text-primary);
margin: 0;
}
.user-info {
display: flex;
align-items: center;
gap: 1rem;
}
.btn-logout {
background-color: var(--error-color);
color: white;
}
.btn-logout:hover {
background-color: #bb2d3b;
}
/* 页面内容区域 */
.page {
display: none;
animation: fadeIn 0.3s ease-in-out;
padding: 1.5rem;
}
.page.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: var(--card-bg);
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.3s, box-shadow 0.3s;
border: 1px solid rgba(0, 0, 0, 0.125);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
}
.stat-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.stat-label {
color: var(--text-secondary);
font-size: 0.875rem;
}
/* 表格样式覆盖 */
.table-container {
background-color: var(--card-bg);
border-radius: 8px;
overflow: hidden;
}
/* 状态标签 */
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
/* 自定义Toast消息提示 */
.custom-toast {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 5px;
color: white;
z-index: 10000;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.custom-toast.success { background-color: var(--success-color); }
.custom-toast.error { background-color: var(--error-color); }
.custom-toast.warning { background-color: var(--warning-color); }
.custom-toast.info { background-color: var(--info-color); }
/* 文件管理相关样式 */
.file-toolbar {
background: var(--card-bg);
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.file-list-container {
background: var(--card-bg);
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 var(--border-color);
cursor: pointer;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.selected {
background-color: #e3f2fd;
}
.file-item:last-child {
border-bottom: none;
}
.file-icon {
width: 24px;
height: 24px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 500;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-meta {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
margin-left: 12px;
}
.file-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 15px;
font-size: 14px;
padding: 10px 0;
flex-wrap: wrap;
}
.breadcrumb-item {
cursor: pointer;
color: var(--primary-color);
white-space: nowrap;
}
.breadcrumb-item:hover {
text-decoration: underline;
}
.file-search-box {
flex: 1;
min-width: 200px;
}
.upload-area {
border: 2px dashed var(--border-color);
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background-color 0.2s;
margin-bottom: 15px;
}
.upload-area:hover {
border-color: var(--primary-color);
background-color: rgba(13, 110, 253, 0.02);
}
.upload-area.dragover {
border-color: var(--primary-color);
background-color: rgba(13, 110, 253, 0.05);
}
.file-actions .btn {
padding: 4px 8px;
font-size: 12px;
min-width: auto;
}
/* 加载和空状态显示 */
.loading {
padding: 40px 20px;
text-align: center;
color: var(--text-secondary);
}
.empty-state {
padding: 40px 20px;
text-align: center;
color: var(--text-secondary);
}
/* 修复面包屑分隔符显示 */
.file-breadcrumb span:not(.breadcrumb-item) {
color: var(--text-secondary);
margin: 0 4px;
}
/* 小屏幕优化 */
@media (max-width: 768px) {
.file-item {
padding: 8px 12px;
}
.file-actions {
flex-direction: column;
gap: 4px;
margin-left: 8px;
}
.file-actions .btn {
padding: 2px 6px;
font-size: 11px;
}
.file-name {
font-size: 14px;
}
.file-meta {
font-size: 11px;
}
.sidebar {
width: 100%;
height: auto;
position: relative;
}
.main-content {
height: 100vh;
}
.content-header {
padding: 1rem;
}
.page {
padding: 1rem;
}
}
/* 大屏幕优化 */
@media (min-width: 769px) {
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(6, 1fr);
}
}
/* 加载动画 */
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 空状态 */
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
</style>
</head>
<body>
<!-- 登录页面 -->
<div class="login-container" id="loginPage">
<div class="login-card">
<div class="login-header text-center mb-4">
<h1>🔐 管理后台</h1>
<p class="text-muted">Qwen Agent 系统管理</p>
</div>
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required placeholder="请输入用户名">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit" class="btn btn-primary w-100" id="loginBtn">登录</button>
</form>
</div>
</div>
<!-- 管理后台主界面 -->
<div class="admin-container h-100" id="adminContainer">
<div class="container-fluid h-100 p-0">
<div class="row h-100 g-0">
<!-- 侧边栏 -->
<div class="col-md-3 col-lg-2 d-md-block">
<aside class="sidebar h-100">
<div class="sidebar-header">
<h2>🎛️ 控制面板</h2>
</div>
<nav class="sidebar-nav">
<button class="nav-item active w-100 text-start" data-page="dashboard">
<i class="bi bi-speedometer2 me-2"></i> 系统概览
</button>
<button class="nav-item w-100 text-start" data-page="queue">
<i class="bi bi-clock-history me-2"></i> 队列管理
</button>
<button class="nav-item w-100 text-start" data-page="files">
<i class="bi bi-folder me-2"></i> 文件管理
</button>
<button class="nav-item w-100 text-start" data-page="projects">
<i class="bi bi-collection me-2"></i> 项目管理
</button>
<button class="nav-item w-100 text-start" data-page="settings">
<i class="bi bi-gear me-2"></i> 系统设置
</button>
</nav>
</aside>
</div>
<!-- 主内容区 -->
<div class="col-md-9 col-lg-10">
<main class="main-content h-100">
<header class="content-header">
<div class="d-flex justify-content-between align-items-center">
<h1 id="pageTitle" class="h3 mb-0">系统概览</h1>
<div class="user-info">
<span class="me-3">
<i class="bi bi-person-circle me-1"></i> <span id="currentUser">admin</span>
</span>
<button class="btn btn-outline-danger btn-sm" onclick="logout()">
<i class="bi bi-box-arrow-right me-1"></i> 退出登录
</button>
</div>
</div>
</header>
<!-- 仪表板页面 -->
<div class="page active" id="dashboard">
<div class="stats-grid">
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-icon text-primary mb-2">
<i class="bi bi-list-check" style="font-size: 2.5rem;"></i>
</div>
<div class="stat-value h2 fw-bold" id="totalTasks">-</div>
<div class="stat-label text-muted small">总任务数</div>
</div>
</div>
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-icon text-warning mb-2">
<i class="bi bi-clock" style="font-size: 2.5rem;"></i>
</div>
<div class="stat-value h2 fw-bold" id="pendingTasks">-</div>
<div class="stat-label text-muted small">待处理</div>
</div>
</div>
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-icon text-info mb-2">
<i class="bi bi-arrow-repeat" style="font-size: 2.5rem;"></i>
</div>
<div class="stat-value h2 fw-bold" id="processingTasks">-</div>
<div class="stat-label text-muted small">处理中</div>
</div>
</div>
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-icon text-success mb-2">
<i class="bi bi-check-circle" style="font-size: 2.5rem;"></i>
</div>
<div class="stat-value h2 fw-bold" id="completedTasks">-</div>
<div class="stat-label text-muted small">已完成</div>
</div>
</div>
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-icon text-danger mb-2">
<i class="bi bi-x-circle" style="font-size: 2.5rem;"></i>
</div>
<div class="stat-value h2 fw-bold" id="failedTasks">-</div>
<div class="stat-label text-muted small">失败</div>
</div>
</div>
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-icon text-secondary mb-2">
<i class="bi bi-folder" style="font-size: 2.5rem;"></i>
</div>
<div class="stat-value h2 fw-bold" id="totalProjects">-</div>
<div class="stat-label text-muted small">项目总数</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-fire me-2"></i> 最近任务
</h5>
</div>
<div class="card-body">
<div id="recentTasks" class="loading">加载中...</div>
</div>
</div>
</div>
<!-- 队列管理页面 -->
<div class="page" id="queue">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-clock-history me-2"></i> 任务队列
</h5>
</div>
<div class="card-body">
<div id="queueContent" class="loading">加载中...</div>
</div>
</div>
</div>
<!-- 文件管理页面 -->
<div class="page" id="files">
<!-- 文件管理工具栏 -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-folder me-2"></i> 项目文件管理器
</h5>
</div>
<div class="card-body">
<div class="file-toolbar">
<div class="file-search-box">
<input type="text" class="form-control" id="fileSearchInput" placeholder="搜索文件..." />
</div>
<div class="btn-group" role="group">
<button class="btn btn-primary" onclick="showUploadModal()">
<i class="bi bi-upload me-1"></i> 上传文件
</button>
<button class="btn btn-success" onclick="showCreateFolderModal()">
<i class="bi bi-folder-plus me-1"></i> 新建文件夹
</button>
<button class="btn btn-secondary" onclick="refreshFileList()">
<i class="bi bi-arrow-clockwise me-1"></i> 刷新
</button>
<button class="btn btn-info" onclick="toggleBatchMode()" id="batchModeBtn">
<i class="bi bi-check-square me-1"></i> 批量操作
</button>
<button class="btn btn-primary" onclick="selectAll()" id="selectAllBtn" style="display: none;">
<i class="bi bi-check-all me-1"></i> 全选
</button>
<button class="btn btn-success" onclick="downloadSelected()" id="downloadSelectedBtn" style="display: none;">
<i class="bi bi-download me-1"></i> 下载选中
</button>
</div>
</div>
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb" class="file-breadcrumb" id="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="#" onclick="navigateToPath('')">
<i class="bi bi-house me-1"></i> 根目录
</a>
</li>
</ol>
</nav>
<!-- 文件列表 -->
<div class="file-list-container border rounded">
<div id="fileList">
<div class="loading">加载中...</div>
</div>
</div>
</div>
</div>
</div>
<!-- 项目管理页面 -->
<div class="page" id="projects">
<div class="row">
<!-- 机器人项目 -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-robot me-2"></i> 机器人项目
</h5>
</div>
<div class="card-body">
<div id="robotProjectsContent" class="loading">加载中...</div>
</div>
</div>
</div>
<!-- 数据集 -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-database me-2"></i> 数据集
</h5>
</div>
<div class="card-body">
<div id="datasetsContent" class="loading">加载中...</div>
</div>
</div>
</div>
</div>
</div>
<!-- 系统设置页面 -->
<div class="page" id="settings">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-gear me-2"></i> 系统设置
</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
系统设置功能正在开发中...
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- 文件上传模态框 -->
<div class="modal fade" id="uploadModal" tabindex="-1" aria-labelledby="uploadModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="uploadModalLabel">
<i class="bi bi-upload me-2"></i>上传文件
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="upload-area" id="uploadArea">
<p class="mb-0">📁 拖拽文件到这里或点击选择文件</p>
<input type="file" id="fileInput" multiple style="display: none;" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<!-- 新建文件夹模态框 -->
<div class="modal fade" id="createFolderModal" tabindex="-1" aria-labelledby="createFolderModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createFolderModalLabel">
<i class="bi bi-folder-plus me-2"></i>新建文件夹
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="folderNameInput" class="form-label">文件夹名称:</label>
<input type="text" class="form-control" id="folderNameInput" 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="createFolder()">创建</button>
</div>
</div>
</div>
</div>
<!-- 重命名模态框 -->
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModalLabel">
<i class="bi bi-pencil me-2"></i>重命名
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="renameInput" class="form-label">新名称:</label>
<input type="text" class="form-control" id="renameInput" 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="renameItem()">重命名</button>
</div>
</div>
</div>
</div>
<script>
// 全局变量
const API_BASE = `${window.location.protocol}//${window.location.host}/api/v1`;
const FILE_API_BASE = `${window.location.protocol}//${window.location.host}/api/v1/files`;
let isLoggedIn = false;
let currentPage = 'dashboard';
let refreshInterval = null;
// 文件管理相关变量
let currentPath = '';
let selectedItems = new Set();
let currentItemToRename = '';
let batchMode = false;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
// 检查登录状态
checkLoginStatus();
// 设置事件监听器
setupEventListeners();
});
// 设置事件监听器
function setupEventListeners() {
// 登录表单
document.getElementById('loginForm').addEventListener('submit', handleLogin);
// 导航按钮
document.querySelectorAll('.nav-item').forEach(btn => {
btn.addEventListener('click', function() {
const page = this.dataset.page;
if (page) {
navigateToPage(page);
}
});
});
// 文件管理事件监听器
setupFileEventListeners();
}
// 设置文件管理事件监听器
function setupFileEventListeners() {
// 搜索功能
const searchInput = document.getElementById('fileSearchInput');
if (searchInput) {
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');
if (uploadArea && 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);
});
}
// Bootstrap模态框会自动处理点击外部关闭
// 但我们仍需要处理文件上传区域的事件
}
// 检查登录状态
function checkLoginStatus() {
const token = localStorage.getItem('admin_token');
const username = localStorage.getItem('admin_username');
if (token && username) {
// 验证token是否有效这里简单检查实际应该调用API验证
isLoggedIn = true;
document.getElementById('currentUser').textContent = username;
showAdminPanel();
} else {
showLoginPage();
}
}
// 处理登录
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
// 禁用登录按钮
loginBtn.disabled = true;
loginBtn.textContent = '登录中...';
try {
// 简单的登录验证实际应该调用后端API
if (username === 'admin' && password === 'admin123') {
// 模拟登录成功
localStorage.setItem('admin_token', 'mock_token_' + Date.now());
localStorage.setItem('admin_username', username);
isLoggedIn = true;
document.getElementById('currentUser').textContent = username;
showMessage('登录成功', 'success');
setTimeout(() => {
showAdminPanel();
}, 1000);
} else {
showMessage('用户名或密码错误', 'error');
}
} catch (error) {
console.error('登录失败:', error);
showMessage('登录失败,请重试', 'error');
} finally {
// 恢复登录按钮
loginBtn.disabled = false;
loginBtn.textContent = '登录';
}
}
// 退出登录
function logout() {
if (confirm('确定要退出登录吗?')) {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_username');
isLoggedIn = false;
// 停止定时刷新
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
showMessage('已退出登录', 'info');
showLoginPage();
}
}
// 显示登录页面
function showLoginPage() {
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('adminContainer').style.display = 'none';
// 清空表单
document.getElementById('loginForm').reset();
}
// 显示管理后台
function showAdminPanel() {
document.getElementById('loginPage').style.display = 'none';
document.getElementById('adminContainer').style.display = 'block';
// 加载仪表板数据
loadDashboardData();
// 设置定时刷新
refreshInterval = setInterval(() => {
if (currentPage === 'dashboard') {
loadDashboardData();
} else if (currentPage === 'queue') {
loadQueueData();
}
}, 30000); // 每30秒刷新一次
}
// 导航到页面
function navigateToPage(page) {
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-page="${page}"]`).classList.add('active');
// 隐藏所有页面
document.querySelectorAll('.page').forEach(p => {
p.classList.remove('active');
});
// 显示目标页面
document.getElementById(page).classList.add('active');
currentPage = page;
// 更新页面标题
const titles = {
dashboard: '系统概览',
queue: '队列管理',
files: '文件管理',
projects: '项目管理',
settings: '系统设置'
};
document.getElementById('pageTitle').textContent = titles[page] || '管理后台';
// 加载页面数据
loadPageData(page);
}
// 加载页面数据
function loadPageData(page) {
switch (page) {
case 'dashboard':
loadDashboardData();
break;
case 'queue':
loadQueueData();
break;
case 'files':
loadFileList();
break;
case 'projects':
loadProjectsData();
break;
}
}
// 加载仪表板数据
async function loadDashboardData() {
try {
// 加载任务统计
await loadTaskStats();
// 加载最近任务
await loadRecentTasks();
// 加载项目统计
await loadProjectStats();
} catch (error) {
console.error('加载仪表板数据失败:', error);
showMessage('加载数据失败', 'error');
}
}
// 加载任务统计
async function loadTaskStats() {
try {
const response = await fetch(`${API_BASE}/tasks/statistics`);
const data = await response.json();
if (data.success) {
const stats = data.statistics;
const statusBreakdown = stats.status_breakdown || {};
document.getElementById('totalTasks').textContent = stats.total_tasks || 0;
document.getElementById('pendingTasks').textContent = statusBreakdown.pending || 0;
document.getElementById('processingTasks').textContent = statusBreakdown.processing || 0;
document.getElementById('completedTasks').textContent = statusBreakdown.completed || 0;
document.getElementById('failedTasks').textContent = statusBreakdown.failed || 0;
}
} catch (error) {
console.error('加载任务统计失败:', error);
}
}
// 加载最近任务
async function loadRecentTasks() {
try {
const response = await fetch(`${API_BASE}/tasks?limit=10`);
const data = await response.json();
const container = document.getElementById('recentTasks');
if (data.success && data.tasks && data.tasks.length > 0) {
const html = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>任务ID</th>
<th>项目ID</th>
<th>状态</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
${data.tasks.map(task => `
<tr>
<td><code>${task.task_id || '-'}</code></td>
<td><code>${task.unique_id || '-'}</code></td>
<td>${getStatusBadge(task.status)}</td>
<td>${formatDate(task.created_at)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = html;
} else {
container.innerHTML = '<div class="empty-state text-center py-5"><div class="empty-state-icon fs-1 mb-3">📋</div><p class="text-muted">暂无任务记录</p></div>';
}
} catch (error) {
console.error('加载最近任务失败:', error);
document.getElementById('recentTasks').innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
}
}
// 加载项目统计
async function loadProjectStats() {
try {
const [robotResponse, datasetResponse] = await Promise.all([
fetch(`${API_BASE}/projects/robot`),
fetch(`${API_BASE}/projects/datasets`)
]);
const robotData = await robotResponse.json();
const datasetData = await datasetResponse.json();
let totalProjects = 0;
if (robotData.success) {
totalProjects += robotData.projects ? robotData.projects.length : 0;
}
if (datasetData.success) {
totalProjects += datasetData.projects ? datasetData.projects.length : 0;
}
document.getElementById('totalProjects').textContent = totalProjects;
} catch (error) {
console.error('加载项目统计失败:', error);
document.getElementById('totalProjects').textContent = '0';
}
}
// 加载队列数据
async function loadQueueData() {
try {
const response = await fetch(`${API_BASE}/tasks`);
const data = await response.json();
const container = document.getElementById('queueContent');
if (data.success && data.tasks) {
const html = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>任务ID</th>
<th>项目ID</th>
<th>状态</th>
<th>创建时间</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${data.tasks.map(task => `
<tr>
<td><code>${task.task_id || '-'}</code></td>
<td><code>${task.unique_id || '-'}</code></td>
<td>${getStatusBadge(task.status)}</td>
<td>${formatDate(task.created_at)}</td>
<td>${formatDate(task.updated_at)}</td>
<td>
<div class="btn-group" role="group">
${task.status === 'failed' ? `<button class="btn btn-primary btn-sm" onclick="retryTask('${task.task_id}')"><i class="bi bi-arrow-clockwise me-1"></i>重试</button>` : ''}
${task.status === 'pending' ? `<button class="btn btn-warning btn-sm" onclick="cancelTask('${task.task_id}')"><i class="bi bi-x-circle me-1"></i>取消</button>` : ''}
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = html;
} else {
container.innerHTML = '<div class="empty-state text-center py-5"><div class="empty-state-icon fs-1 mb-3">⏳</div><p class="text-muted">队列为空</p></div>';
}
} catch (error) {
console.error('加载队列数据失败:', error);
document.getElementById('queueContent').innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
}
}
// 加载项目数据
async function loadProjectsData() {
try {
// 加载机器人项目
await loadRobotProjects();
// 加载数据集
await loadDatasets();
} catch (error) {
console.error('加载项目数据失败:', error);
document.getElementById('robotProjectsContent').innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
document.getElementById('datasetsContent').innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
}
}
// 加载机器人项目
async function loadRobotProjects() {
try {
const response = await fetch(`${API_BASE}/projects/robot`);
const data = await response.json();
const container = document.getElementById('robotProjectsContent');
if (data.success && data.projects && data.projects.length > 0) {
const html = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>项目ID</th>
<th>名称</th>
<th>文件数量</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${data.projects.map(project => `
<tr>
<td><code>${project.id}</code></td>
<td>${project.name || '-'}</td>
<td><span class="badge bg-secondary">${project.file_count || 0}</span></td>
<td>${getProjectStatusBadge(project.status)}</td>
<td>${formatDate(project.created_at)}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-outline-primary btn-sm" onclick="viewRobotProject('${project.id}')"><i class="bi bi-eye me-1"></i>查看</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteRobotProject('${project.id}')"><i class="bi bi-trash me-1"></i>删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = html;
} else {
container.innerHTML = '<div class="empty-state text-center py-5"><div class="empty-state-icon fs-1 mb-3">🤖</div><p class="text-muted">暂无机器人项目</p></div>';
}
} catch (error) {
console.error('加载机器人项目失败:', error);
document.getElementById('robotProjectsContent').innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
}
}
// 加载数据集
async function loadDatasets() {
try {
const response = await fetch(`${API_BASE}/projects/datasets`);
const data = await response.json();
const container = document.getElementById('datasetsContent');
if (data.success && data.projects && data.projects.length > 0) {
const html = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>数据集ID</th>
<th>名称</th>
<th>文件数量</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${data.projects.map(dataset => `
<tr>
<td><code>${dataset.id}</code></td>
<td>${dataset.name || '-'}</td>
<td><span class="badge bg-secondary">${dataset.file_count || 0}</span></td>
<td>${getDatasetStatusBadge(dataset.status)}</td>
<td>${formatDate(dataset.created_at)}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-outline-primary btn-sm" onclick="viewDataset('${dataset.id}')"><i class="bi bi-eye me-1"></i>查看</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteDataset('${dataset.id}')"><i class="bi bi-trash me-1"></i>删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = html;
} else {
container.innerHTML = '<div class="empty-state text-center py-5"><div class="empty-state-icon fs-1 mb-3">📊</div><p class="text-muted">暂无数据集</p></div>';
}
} catch (error) {
console.error('加载数据集失败:', error);
document.getElementById('datasetsContent').innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
}
}
// 工具函数
function getStatusText(status) {
const statusMap = {
'pending': '待处理',
'processing': '处理中',
'completed': '已完成',
'failed': '失败',
'cancelled': '已取消'
};
return statusMap[status] || status;
}
// Bootstrap 状态标签
function getStatusBadge(status) {
const statusMap = {
'pending': '<span class="badge bg-warning">待处理</span>',
'processing': '<span class="badge bg-info">处理中</span>',
'completed': '<span class="badge bg-success">已完成</span>',
'failed': '<span class="badge bg-danger">失败</span>',
'cancelled': '<span class="badge bg-secondary">已取消</span>'
};
return statusMap[status] || `<span class="badge bg-secondary">${status}</span>`;
}
function getProjectStatusBadge(status) {
const statusMap = {
'active': '<span class="badge bg-success">活跃</span>',
'completed': '<span class="badge bg-primary">已完成</span>',
'failed': '<span class="badge bg-danger">失败</span>',
'unknown': '<span class="badge bg-secondary">未知</span>'
};
return statusMap[status] || `<span class="badge bg-secondary">${status}</span>`;
}
function getDatasetStatusBadge(status) {
const statusMap = {
'active': '<span class="badge bg-success">活跃</span>',
'completed': '<span class="badge bg-primary">已完成</span>',
'processing': '<span class="badge bg-info">处理中</span>',
'failed': '<span class="badge bg-danger">失败</span>',
'unknown': '<span class="badge bg-secondary">未知</span>'
};
return statusMap[status] || `<span class="badge bg-secondary">${status}</span>`;
}
function getProjectStatusText(status) {
const statusMap = {
'active': '活跃',
'completed': '已完成',
'failed': '失败',
'unknown': '未知'
};
return statusMap[status] || status;
}
function getDatasetStatusText(status) {
const statusMap = {
'active': '活跃',
'completed': '已完成',
'processing': '处理中',
'failed': '失败',
'unknown': '未知'
};
return statusMap[status] || status;
}
function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('zh-CN');
}
function showMessage(message, type) {
const toast = document.createElement('div');
toast.className = `custom-toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 4000);
}
// 任务操作函数
async function retryTask(taskId) {
try {
showMessage(`正在重试任务 ${taskId}...`, 'info');
// 这里可以调用实际的API来重试任务
// const response = await fetch(`${API_BASE}/tasks/${taskId}/retry`, { method: 'POST' });
showMessage(`任务 ${taskId} 已重新提交`, 'success');
loadQueueData();
} catch (error) {
console.error('重试任务失败:', error);
showMessage(`重试任务失败`, 'error');
}
}
async function cancelTask(taskId) {
if (confirm(`确定要取消任务 ${taskId} 吗?`)) {
try {
// 这里可以调用实际的API来取消任务
// const response = await fetch(`${API_BASE}/tasks/${taskId}/cancel`, { method: 'POST' });
showMessage(`已取消任务 ${taskId}`, 'success');
loadQueueData();
} catch (error) {
console.error('取消任务失败:', error);
showMessage(`取消任务失败`, 'error');
}
}
}
function viewProject(projectId) {
// 切换到队列管理页面并筛选该项目的任务
navigateToPage('queue');
showMessage(`正在查看项目 ${projectId} 的任务`, 'info');
}
function viewRobotProject(projectId) {
navigateToPage('queue');
showMessage(`正在查看机器人项目 ${projectId} 的任务`, 'info');
}
function viewDataset(datasetId) {
navigateToPage('queue');
showMessage(`正在查看数据集 ${datasetId} 的任务`, 'info');
}
async function deleteProject(projectId) {
if (confirm(`确定要删除项目 ${projectId} 吗?此操作不可撤销!`)) {
try {
showMessage(`正在删除项目 ${projectId}...`, 'info');
// 这里可以调用实际的API来删除项目
// const response = await fetch(`${API_BASE}/projects/${projectId}`, { method: 'DELETE' });
showMessage(`已删除项目 ${projectId}`, 'success');
loadProjectsData();
} catch (error) {
console.error('删除项目失败:', error);
showMessage(`删除项目失败`, 'error');
}
}
}
async function deleteRobotProject(projectId) {
if (confirm(`确定要删除机器人项目 ${projectId} 吗?此操作不可撤销!`)) {
try {
showMessage(`正在删除机器人项目 ${projectId}...`, 'info');
// 这里可以调用实际的API来删除项目
// const response = await fetch(`${API_BASE}/projects/robot/${projectId}`, { method: 'DELETE' });
showMessage(`已删除机器人项目 ${projectId}`, 'success');
loadRobotProjects();
} catch (error) {
console.error('删除机器人项目失败:', error);
showMessage(`删除机器人项目失败`, 'error');
}
}
}
async function deleteDataset(datasetId) {
if (confirm(`确定要删除数据集 ${datasetId} 吗?此操作不可撤销!`)) {
try {
showMessage(`正在删除数据集 ${datasetId}...`, 'info');
// 这里可以调用实际的API来删除数据集
// const response = await fetch(`${API_BASE}/projects/datasets/${datasetId}`, { method: 'DELETE' });
showMessage(`已删除数据集 ${datasetId}`, 'success');
loadDatasets();
} catch (error) {
console.error('删除数据集失败:', error);
showMessage(`删除数据集失败`, 'error');
}
}
}
// ========== 文件管理功能 ==========
// 加载文件列表
async function loadFileList(path = '') {
try {
const response = await fetch(`${FILE_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');
if (!fileList) return;
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 fw-bold">${item.name}</div>
<div class="file-meta text-muted small">${size}${date}</div>
</div>
<div class="file-actions">
<div class="btn-group" role="group">
${item.type === 'file' ?
`<button class="btn btn-outline-primary btn-sm" onclick="downloadFile('${item.path}')"><i class="bi bi-download"></i></button>` :
`<button class="btn btn-outline-primary btn-sm" onclick="downloadFolderAsZip('${item.path}')"><i class="bi bi-file-zip"></i></button>`
}
<button class="btn btn-outline-secondary btn-sm" onclick="showRenameModal('${item.path}')"><i class="bi bi-pencil"></i></button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteItem('${item.path}')"><i class="bi bi-trash"></i></button>
</div>
</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(`${FILE_API_BASE}/download/${item.path}`, '_blank');
}
}
});
// Bootstrap点击外部关闭模态框的兼容处理
div.addEventListener('contextmenu', (e) => {
e.preventDefault();
// 这里可以添加右键菜单功能
});
// 添加选中状态的样式
if (isSelected) {
div.classList.add('selected');
}
return div;
}
// 获取文件图标
function getFileIcon(fileName) {
const ext = fileName.split('.').pop().toLowerCase();
const iconMap = {
'md': '<i class="bi bi-file-text"></i>',
'txt': '<i class="bi bi-file-earmark-text"></i>',
'pdf': '<i class="bi bi-file-pdf"></i>',
'doc': '<i class="bi bi-file-word"></i>',
'docx': '<i class="bi bi-file-word"></i>',
'xls': '<i class="bi bi-file-excel"></i>',
'xlsx': '<i class="bi bi-file-excel"></i>',
'ppt': '<i class="bi bi-file-ppt"></i>',
'pptx': '<i class="bi bi-file-ppt"></i>',
'jpg': '<i class="bi bi-file-image"></i>',
'jpeg': '<i class="bi bi-file-image"></i>',
'png': '<i class="bi bi-file-image"></i>',
'gif': '<i class="bi bi-file-image"></i>',
'zip': '<i class="bi bi-file-zip"></i>',
'rar': '<i class="bi bi-file-zip"></i>',
'mp4': '<i class="bi bi-file-play"></i>',
'mp3': '<i class="bi bi-file-music"></i>',
'json': '<i class="bi bi-file-code"></i>',
'xml': '<i class="bi bi-file-code"></i>',
'html': '<i class="bi bi-file-code"></i>',
'css': '<i class="bi bi-file-code"></i>',
'js': '<i class="bi bi-file-code"></i>',
'py': '<i class="bi bi-file-code"></i>'
};
return iconMap[ext] || '<i class="bi bi-file-earmark"></i>';
}
// 格式化文件大小
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');
if (!breadcrumb) return;
let breadcrumbHtml = '<ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="#" onclick="navigateToPath(\'\')"><i class="bi bi-house me-1"></i>根目录</a></li>';
if (path) {
const parts = path.split('/');
let currentPath = '';
parts.forEach((part, index) => {
currentPath += (currentPath ? '/' : '') + part;
const isLast = index === parts.length - 1;
if (!isLast) {
breadcrumbHtml += `<li class="breadcrumb-item"><a href="#" onclick="navigateToPath('${currentPath}')">${part}</a></li>`;
} else {
breadcrumbHtml += `<li class="breadcrumb-item active" aria-current="page">${part}</li>`;
}
});
}
breadcrumbHtml += '</ol>';
breadcrumb.innerHTML = breadcrumbHtml;
}
// 导航到路径
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(`${FILE_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(`${FILE_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 uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('path', currentPath);
try {
const response = await fetch(`${FILE_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(`${FILE_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(`${FILE_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(`${FILE_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(`${FILE_API_BASE}/download/${path}`, '_blank');
}
// 下载文件夹为ZIP
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(`${FILE_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');
if (downloadBtn) downloadBtn.style.display = 'inline-block';
if (selectAllBtn) selectAllBtn.style.display = 'inline-block';
selectedItems.clear();
} else {
batchBtn.textContent = '📦 批量操作';
batchBtn.classList.remove('btn-secondary');
batchBtn.classList.add('btn-info');
if (downloadBtn) downloadBtn.style.display = 'none';
if (selectAllBtn) 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);
const selectAllBtn = document.getElementById('selectAllBtn');
if (selectedItems.size === allPaths.length) {
// 如果已全选,则取消全选
selectedItems.clear();
if (selectAllBtn) selectAllBtn.textContent = '☑️ 全选';
} else {
// 全选
selectedItems.clear();
allPaths.forEach(path => selectedItems.add(path));
if (selectAllBtn) 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();
}
// 更新批量操作UI
function updateBatchUI() {
const downloadBtn = document.getElementById('downloadSelectedBtn');
const count = selectedItems.size;
if (downloadBtn) {
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 => {
// 这里可以检查是否为文件夹,简化处理
return false;
});
if (hasFolders) {
showMessage('检测到文件夹,正在创建压缩包...', 'info');
await downloadItemsAsZip(items);
} else {
// 下载单个文件
for (const item of items) {
downloadFile(item);
}
}
}
// 批量下载项目为ZIP
async function downloadItemsAsZip(items) {
try {
showMessage(`正在压缩 ${items.length} 个项目...`, 'info');
const response = await fetch(`${FILE_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';
}
// 显示模态框 (使用Bootstrap)
function showUploadModal() {
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
modal.show();
}
function showCreateFolderModal() {
const modal = new bootstrap.Modal(document.getElementById('createFolderModal'));
modal.show();
}
function showRenameModal(path) {
currentItemToRename = path;
const fileName = path.split('/').pop();
document.getElementById('renameInput').value = fileName;
const modal = new bootstrap.Modal(document.getElementById('renameModal'));
modal.show();
}
// 关闭模态框
function closeModal(modalId) {
const modal = bootstrap.Modal.getInstance(document.getElementById(modalId));
if (modal) {
modal.hide();
}
}
// 创建文件夹成功后关闭模态框
function createFolder() {
// 保持原有逻辑添加Bootstrap模态框关闭
const folderName = document.getElementById('folderNameInput').value.trim();
if (!folderName) {
showMessage('请输入文件夹名称', 'error');
return;
}
fetch(`${FILE_API_BASE}/create-folder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: currentPath,
name: folderName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage('文件夹创建成功', 'success');
closeModal('createFolderModal');
loadFileList();
} else {
showMessage('文件夹创建失败', 'error');
}
})
.catch(error => {
console.error('创建文件夹失败:', error);
showMessage('创建文件夹失败', 'error');
});
}
// 重命名成功后关闭模态框
function renameItem() {
const newName = document.getElementById('renameInput').value.trim();
if (!newName) {
showMessage('请输入新名称', 'error');
return;
}
fetch(`${FILE_API_BASE}/rename`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
old_path: currentItemToRename,
new_name: newName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage('重命名成功', 'success');
closeModal('renameModal');
loadFileList();
} else {
showMessage('重命名失败', 'error');
}
})
.catch(error => {
console.error('重命名失败:', error);
showMessage('重命名失败', 'error');
});
}
</script>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>