update web dataset file

This commit is contained in:
wangyuzhan 2025-11-21 10:41:05 +08:00
parent 4a741b3d3b
commit a0013eae8f
4 changed files with 530 additions and 21 deletions

View File

@ -366,6 +366,232 @@ input[type="text"]:focus, input[type="number"]:focus, select:focus, textarea:foc
font-weight: 600;
color: var(--accent);
}
/* 文件浏览器模态框样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
transition: opacity 0.3s ease;
}
.modal.hidden {
display: none;
}
.modal-content {
background: var(--panel-bg);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
box-shadow: var(--shadow-xl);
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(139, 92, 246, 0.1);
}
.modal-header h3 {
margin: 0;
color: var(--text);
font-size: 1.1rem;
}
.modal-close {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
transition: all 0.3s ease;
}
.modal-close:hover {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.modal-body {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
display: flex;
gap: 0.5rem;
align-items: center;
background: rgba(22, 22, 32, 0.8);
}
.modal-footer input {
flex: 1;
}
.btn-secondary {
background: linear-gradient(135deg, var(--border-light) 0%, var(--border) 100%);
color: var(--text);
border: none;
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.8rem;
font-weight: 500;
}
.btn-secondary:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
/* 文件浏览器导航 */
.file-browser-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 0.5rem;
background: rgba(45, 55, 72, 0.5);
border-radius: var(--radius-md);
}
.current-path {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: var(--text-secondary);
flex: 1;
margin-right: 1rem;
padding: 0.25rem 0.5rem;
background: rgba(22, 22, 32, 0.5);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}
.btn-navigate {
background: linear-gradient(135deg, var(--info-grad-start) 0%, var(--info-grad-end) 100%);
color: white;
border: none;
padding: 0.5rem;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
transition: all 0.3s ease;
}
.btn-navigate:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
/* 快捷路径 */
.quick-paths {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.quick-path-btn {
background: rgba(139, 92, 246, 0.1);
color: var(--accent);
border: 1px solid rgba(139, 92, 246, 0.3);
padding: 0.4rem 0.8rem;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.75rem;
transition: all 0.3s ease;
}
.quick-path-btn:hover {
background: rgba(139, 92, 246, 0.2);
transform: translateY(-1px);
}
/* 文件列表 */
.file-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: rgba(22, 22, 32, 0.3);
}
.file-item {
display: flex;
align-items: center;
padding: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 1px solid rgba(45, 55, 72, 0.3);
}
.file-item:last-child {
border-bottom: none;
}
.file-item:hover {
background: rgba(139, 92, 246, 0.1);
}
.file-item.selected {
background: rgba(139, 92, 246, 0.2);
border-left: 3px solid var(--accent);
}
.file-icon {
margin-right: 0.75rem;
font-size: 1.2rem;
width: 20px;
text-align: center;
}
.file-name {
flex: 1;
font-size: 0.9rem;
color: var(--text);
}
.file-info {
font-size: 0.75rem;
color: var(--text-secondary);
text-align: right;
}
.directory {
color: var(--info-grad-start);
}
.file {
color: var(--text-secondary);
}
.checkbox-group {
display: flex;
align-items: center;

View File

@ -17,37 +17,182 @@ const hooks = {
window.openTab = (evt, tabName) => _openTab(evt, tabName, hooks);
// 文件夹选择器功能
// 文件夹选择器功能 - 支持远程文件浏览
window.selectFolder = async (inputId) => {
try {
// 检测是否为远程连接(通过检查主机名或端口)
const isRemote = window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1';
if (isRemote) {
// 远程连接:使用服务器端文件浏览
openRemoteFileBrowser(inputId);
} else {
// 本地连接:尝试本地文件系统访问
await openLocalFileBrowser(inputId);
}
} catch (error) {
console.warn('文件夹选择失败:', error);
if (error.name !== 'AbortError') {
// 降级到远程文件浏览
openRemoteFileBrowser(inputId);
}
}
};
// 本地文件浏览器(兼容模式)
async function openLocalFileBrowser(inputId) {
try {
// 检查是否支持 File System Access API
if ('showDirectoryPicker' in window) {
const dirHandle = await window.showDirectoryPicker();
const path = dirHandle.name; // 使用目录名称作为路径
const path = dirHandle.name;
document.getElementById(inputId).value = `./${path}`;
} else {
// 降级方案:使用文件输入模拟
const input = document.createElement('input');
input.type = 'file';
input.webkitdirectory = true;
input.onchange = (e) => {
const files = e.target.files;
if (files.length > 0) {
// 提取相对路径
const path = files[0].webkitRelativePath.split('/')[0];
document.getElementById(inputId).value = `./${path}`;
}
};
input.click();
// 降级到远程文件浏览
await openRemoteFileBrowser(inputId);
}
} catch (error) {
console.warn('文件夹选择失败:', error);
// 如果用户取消选择,不显示错误
if (error.name !== 'AbortError') {
alert('文件夹选择失败,请手动输入路径');
}
// 如果本地失败,降级到远程文件浏览
await openRemoteFileBrowser(inputId);
}
};
}
// 远程文件浏览器
let currentFileBrowserTarget = null;
let currentBrowsePath = './';
function openRemoteFileBrowser(inputId) {
currentFileBrowserTarget = inputId;
document.getElementById('file-browser-modal').classList.remove('hidden');
// 加载初始路径
loadQuickPaths();
browsePath('./');
}
function closeFileBrowser() {
document.getElementById('file-browser-modal').classList.add('hidden');
currentFileBrowserTarget = null;
currentBrowsePath = './';
}
function confirmFileSelection() {
const selectedPath = document.getElementById('selected-path').value;
if (selectedPath && currentFileBrowserTarget) {
document.getElementById(currentFileBrowserTarget).value = selectedPath;
}
closeFileBrowser();
}
function navigateToParent() {
if (currentBrowsePath !== './') {
const parentPath = currentBrowsePath.includes('/') ?
currentBrowsePath.substring(0, currentBrowsePath.lastIndexOf('/')) : './';
browsePath(parentPath || './');
}
}
async function loadQuickPaths() {
try {
const response = await fetch('/api/quick-paths');
const data = await response.json();
const quickPathsContainer = document.getElementById('quick-paths');
quickPathsContainer.innerHTML = '';
if (data.paths && data.paths.length > 0) {
data.paths.forEach(path => {
const btn = document.createElement('button');
btn.className = 'quick-path-btn';
btn.textContent = path.name;
btn.onclick = () => browsePath(path.path);
btn.title = path.path;
quickPathsContainer.appendChild(btn);
});
}
} catch (error) {
console.warn('加载快捷路径失败:', error);
}
}
async function browsePath(path) {
try {
currentBrowsePath = path;
document.getElementById('current-path').textContent = path;
const response = await fetch(`/api/browse?path=${encodeURIComponent(path)}`);
const data = await response.json();
if (data.error) {
alert(`浏览失败: ${data.error}`);
return;
}
renderFileList(data);
} catch (error) {
console.error('浏览路径失败:', error);
alert('浏览路径失败,请检查网络连接');
}
}
function renderFileList(data) {
const fileList = document.getElementById('file-list');
fileList.innerHTML = '';
if (!data.items || data.items.length === 0) {
fileList.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--text-secondary);">此目录为空</div>';
return;
}
// 先显示目录,再显示文件
const directories = data.items.filter(item => item.type === 'directory');
const files = data.items.filter(item => item.type === 'file');
// 渲染目录
directories.forEach(item => {
const div = createFileItem(item, '📁');
fileList.appendChild(div);
});
// 渲染文件
files.forEach(item => {
const div = createFileItem(item, '📄');
fileList.appendChild(div);
});
}
function createFileItem(item, icon) {
const div = document.createElement('div');
div.className = 'file-item';
div.onclick = () => selectFileItem(item);
div.innerHTML = `
<span class="file-icon">${icon}</span>
<span class="file-name">${item.name}</span>
<span class="file-info">${item.type === 'file' ? formatFileSize(item.size) : '文件夹'}</span>
`;
return div;
}
function selectFileItem(item) {
if (item.type === 'directory') {
browsePath(item.path);
} else {
document.getElementById('selected-path').value = item.path;
// 文件被选中,可以高亮显示
document.querySelectorAll('.file-item').forEach(el => el.classList.remove('selected'));
event.currentTarget.classList.add('selected');
}
}
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];
}
window.addEventListener('load', () => {
initTrainForm();

View File

@ -319,6 +319,33 @@
</div>
</div>
<!-- 文件浏览器模态框 -->
<div id="file-browser-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>选择文件夹</h3>
<button class="modal-close" onclick="closeFileBrowser()">&times;</button>
</div>
<div class="modal-body">
<div class="file-browser-nav">
<div class="current-path" id="current-path">./</div>
<button class="btn-navigate" onclick="navigateToParent()">⬆️ 上级目录</button>
</div>
<div class="quick-paths" id="quick-paths">
<!-- 快捷路径将在这里显示 -->
</div>
<div class="file-list" id="file-list">
<!-- 文件列表将在这里显示 -->
</div>
</div>
<div class="modal-footer">
<input type="text" id="selected-path" placeholder="选择的文件夹路径" readonly>
<button class="btn-primary" onclick="confirmFileSelection()">确认选择</button>
<button class="btn-secondary" onclick="closeFileBrowser()">取消</button>
</div>
</div>
</div>
<script type="module" src="/static/js/app.js"></script>
</body>
</html>

View File

@ -11,6 +11,8 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for
from flask import g
import time
import psutil
import glob
import pathlib
# 尝试导入torch来检测GPU
try:
@ -320,6 +322,115 @@ def processes():
})
return jsonify(result)
@app.route('/api/browse')
def browse_files():
"""
浏览服务器文件系统
支持远程文件选择功能
"""
try:
# 获取请求的路径参数
path = request.args.get('path', './')
# 安全检查:限制访问范围
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(script_dir, '..'))
# 解析请求的路径
if path.startswith('./'):
# 相对路径,基于项目根目录
full_path = os.path.abspath(os.path.join(project_root, path[2:]))
elif path.startswith('/'):
# 绝对路径,检查是否在项目目录内
full_path = os.path.abspath(path)
else:
# 相对路径,基于项目根目录
full_path = os.path.abspath(os.path.join(project_root, path))
# 安全检查:确保路径在项目目录内
if not full_path.startswith(project_root):
full_path = project_root
# 检查路径是否存在
if not os.path.exists(full_path):
return jsonify({'error': '路径不存在', 'path': path})
# 获取目录内容
if os.path.isdir(full_path):
items = []
try:
# 列出目录内容
for item in sorted(os.listdir(full_path)):
item_path = os.path.join(full_path, item)
# 跳过隐藏文件和系统文件
if item.startswith('.') or item.startswith('__'):
continue
try:
stat = os.stat(item_path)
items.append({
'name': item,
'path': os.path.relpath(item_path, project_root),
'type': 'directory' if os.path.isdir(item_path) else 'file',
'size': stat.st_size if os.path.isfile(item_path) else 0,
'modified': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime))
})
except (OSError, PermissionError):
# 跳过无法访问的项目
continue
return jsonify({
'current_path': os.path.relpath(full_path, project_root),
'absolute_path': full_path,
'items': items,
'parent': os.path.relpath(os.path.dirname(full_path), project_root) if full_path != project_root else None
})
except (OSError, PermissionError) as e:
return jsonify({'error': f'无法访问目录: {str(e)}', 'path': path})
else:
# 如果是文件,返回文件信息
stat = os.stat(full_path)
return jsonify({
'name': os.path.basename(full_path),
'path': os.path.relpath(full_path, project_root),
'type': 'file',
'size': stat.st_size,
'modified': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime))
})
except Exception as e:
return jsonify({'error': f'浏览文件时出错: {str(e)}'})
@app.route('/api/quick-paths')
def quick_paths():
"""
返回常用路径快捷方式
"""
try:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(script_dir, '..'))
quick_paths = [
{'name': '项目根目录', 'path': './', 'type': 'directory'},
{'name': '数据集目录', 'path': './dataset', 'type': 'directory'},
{'name': '模型检查点', 'path': './checkpoints', 'type': 'directory'},
{'name': '日志文件', 'path': './logfile', 'type': 'directory'}
]
# 验证路径是否存在
valid_paths = []
for item in quick_paths:
full_path = os.path.join(project_root, item['path'][2:] if item['path'].startswith('./') else item['path'])
if os.path.exists(full_path):
valid_paths.append(item)
return jsonify({'paths': valid_paths})
except Exception as e:
return jsonify({'error': f'获取快捷路径时出错: {str(e)}'})
@app.route('/logs/<process_id>')
def logs(process_id):
# 直接从本地logfile目录读取日志文件