2228 lines
89 KiB
HTML
2228 lines
89 KiB
HTML
<!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.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<!-- Bootstrap Icons -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/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>
|