1. 新增了日志系统

2. 新增了添加新训练选择对应的GPU
This commit is contained in:
2026-01-23 11:07:09 +08:00
parent 7f64362826
commit 730ac6f460
6 changed files with 860 additions and 14 deletions

View File

@@ -5,6 +5,7 @@ from .datasets import datasets_bp
from .model_manage import model_manage_bp from .model_manage import model_manage_bp
from .model_chat import model_chat_bp from .model_chat import model_chat_bp
from .dimension import dimension_bp from .dimension import dimension_bp
from .logs import logs_bp
# 注册所有蓝图 # 注册所有蓝图
def register_blueprints(app): def register_blueprints(app):
@@ -13,3 +14,4 @@ def register_blueprints(app):
app.register_blueprint(model_manage_bp) app.register_blueprint(model_manage_bp)
app.register_blueprint(model_chat_bp) app.register_blueprint(model_chat_bp)
app.register_blueprint(dimension_bp) app.register_blueprint(dimension_bp)
app.register_blueprint(logs_bp)

171
src/api/logs.py Normal file
View File

@@ -0,0 +1,171 @@
"""
日志管理 API 路由
"""
import os
import sys
import logging
import yaml
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler, RotatingFileHandler
from flask import Blueprint, request, jsonify
# 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, PROJECT_ROOT)
# 加载配置
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
CONFIG = yaml.safe_load(f)
# 日志目录
LOG_BASE_DIR = os.path.join(PROJECT_ROOT, 'logs')
# 创建蓝图
logs_bp = Blueprint('logs', __name__, url_prefix='/api')
def setup_logs_logger():
"""配置日志系统,按日期分目录存储"""
today = datetime.now().strftime('%Y-%m-%d')
log_dir = os.path.join(LOG_BASE_DIR, today)
os.makedirs(log_dir, exist_ok=True)
logger = logging.getLogger('logs_api')
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
# 全部日志处理器
all_log_path = os.path.join(log_dir, 'all.log')
all_handler = TimedRotatingFileHandler(all_log_path, when='midnight', interval=1, backupCount=30, encoding='utf-8')
all_handler.setLevel(logging.DEBUG)
all_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%H:%M:%S'
))
logger.addHandler(all_handler)
logger.addHandler(console_handler)
return logger
logs_logger = setup_logs_logger()
@logs_bp.route('/web-log', methods=['POST'])
def receive_web_log():
"""接收前端页面发送的日志"""
data = request.json
level = data.get('level', 'info')
message = data.get('message', '')
page = data.get('page', 'unknown')
timestamp = data.get('timestamp', datetime.now().isoformat())
log_message = f'[WEB-{page}] {message}'
if level == 'error':
logs_logger.error(log_message)
elif level == 'warning':
logs_logger.warning(log_message)
elif level == 'debug':
logs_logger.debug(log_message)
else:
logs_logger.info(log_message)
return jsonify({'code': 0, 'message': '日志接收成功'})
def format_file_size(size_bytes):
"""格式化文件大小"""
if size_bytes < 1024:
return f'{size_bytes} B'
elif size_bytes < 1024 * 1024:
return f'{size_bytes / 1024:.1f} KB'
else:
return f'{size_bytes / (1024 * 1024):.1f} MB'
@logs_bp.route('/log-files', methods=['GET'])
def get_log_files():
"""获取指定日期的日志文件列表"""
date = request.args.get('date')
if not date:
return jsonify({'code': 1, 'message': '缺少日期参数'})
# 验证日期格式
try:
datetime.strptime(date, '%Y-%m-%d')
except ValueError:
return jsonify({'code': 1, 'message': '日期格式错误,应为 YYYY-MM-DD'})
log_dir = os.path.join(LOG_BASE_DIR, date)
if not os.path.exists(log_dir):
return jsonify({'code': 0, 'data': []})
log_files = []
file_names = ['all.log', 'error.log', 'request.log']
for file_name in file_names:
file_path = os.path.join(log_dir, file_name)
if os.path.exists(file_path):
size = os.path.getsize(file_path)
log_files.append({
'name': file_name.replace('.log', ''),
'file': f'{date}/{file_name}',
'size': format_file_size(size)
})
return jsonify({'code': 0, 'data': log_files})
@logs_bp.route('/log-content', methods=['GET'])
def get_log_content():
"""获取日志文件内容"""
file_path = request.args.get('file')
if not file_path:
return jsonify({'code': 1, 'message': '缺少文件参数'})
# 防止目录遍历攻击
file_path = file_path.replace('..', '').replace('//', '/')
full_path = os.path.join(LOG_BASE_DIR, file_path)
# 验证文件路径是否在日志目录下
if not full_path.startswith(LOG_BASE_DIR):
return jsonify({'code': 1, 'message': '无效的文件路径'})
if not os.path.exists(full_path) or not os.path.isfile(full_path):
return jsonify({'code': 1, 'message': '日志文件不存在'})
try:
size = os.path.getsize(full_path)
# 限制读取大小,最大 5MB
max_size = 5 * 1024 * 1024
if size > max_size:
with open(full_path, 'r', encoding='utf-8') as f:
f.seek(size - max_size)
content = '... (日志文件较大,已显示最后 5MB 内容) ...\n\n' + f.read()
else:
with open(full_path, 'r', encoding='utf-8') as f:
content = f.read()
return jsonify({
'code': 0,
'data': {
'file': os.path.basename(file_path),
'path': file_path,
'size': format_file_size(size),
'content': content
}
})
except Exception as e:
return jsonify({'code': 1, 'message': f'读取日志文件失败: {str(e)}'})

View File

@@ -7,6 +7,9 @@ import json
import pymysql import pymysql
import yaml import yaml
import time import time
import logging
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler, RotatingFileHandler
from flask import Flask, request, jsonify, send_from_directory from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS from flask_cors import CORS
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -30,6 +33,82 @@ def load_config():
CONFIG = load_config() CONFIG = load_config()
# ============ 日志系统配置 ============
LOG_BASE_DIR = os.path.join(PROJECT_ROOT, 'logs')
def setup_logger(name='app'):
"""配置日志系统,按日期分目录存储"""
# 创建当天的日志目录
today = datetime.now().strftime('%Y-%m-%d')
log_dir = os.path.join(LOG_BASE_DIR, today)
os.makedirs(log_dir, exist_ok=True)
# 获取或创建 logger
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 清除已存在的处理器
logger.handlers.clear()
# 1. 全部日志处理器 (TimedRotatingFileHandler - 每天午夜分割)
all_log_path = os.path.join(log_dir, 'all.log')
all_handler = TimedRotatingFileHandler(all_log_path, when='midnight', interval=1, backupCount=30, encoding='utf-8')
all_handler.setLevel(logging.DEBUG)
all_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# 2. 错误日志处理器 (RotatingFileHandler - 10MB大小分割保留10个备份)
error_log_path = os.path.join(log_dir, 'error.log')
error_handler = RotatingFileHandler(error_log_path, maxBytes=10*1024*1024, backupCount=10, encoding='utf-8')
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# 3. 请求日志处理器
request_log_path = os.path.join(log_dir, 'request.log')
request_handler = RotatingFileHandler(request_log_path, maxBytes=10*1024*1024, backupCount=10, encoding='utf-8')
request_handler.setLevel(logging.INFO)
request_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# 4. 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%H:%M:%S'
))
# 添加处理器到 logger
logger.addHandler(all_handler)
logger.addHandler(error_handler)
logger.addHandler(console_handler)
# 为请求日志创建单独的 logger
request_logger = logging.getLogger('request')
request_logger.setLevel(logging.INFO)
request_logger.handlers.clear()
request_logger.addHandler(request_handler)
request_logger.addHandler(console_handler)
return logger
# 初始化日志系统
logger = setup_logger('app')
request_logger = logging.getLogger('request')
logger.info('=' * 50)
logger.info('服务启动')
logger.info('=' * 50)
def get_db_connection(): def get_db_connection():
"""获取数据库连接""" """获取数据库连接"""
@@ -47,7 +126,7 @@ def get_db_connection():
def init_database(): def init_database():
"""初始化数据库表""" """初始化数据库表"""
print("正在初始化数据库...") logger.info("正在初始化数据库...")
try: try:
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
@@ -60,6 +139,7 @@ def init_database():
base_model VARCHAR(255), base_model VARCHAR(255),
train_type VARCHAR(50), train_type VARCHAR(50),
train_method VARCHAR(50), train_method VARCHAR(50),
gpus JSON COMMENT 'GPU硬件选择支持多卡训练',
dataset_id INT, dataset_id INT,
valid_split VARCHAR(50), valid_split VARCHAR(50),
valid_ratio INT DEFAULT 10, valid_ratio INT DEFAULT 10,
@@ -212,31 +292,31 @@ def init_database():
for i, table_sql in enumerate(tables): for i, table_sql in enumerate(tables):
try: try:
cursor.execute(table_sql) cursor.execute(table_sql)
print(f" {i+1}/{len(tables)} 创建/检查成功") logger.debug(f"{i+1}/{len(tables)} 创建/检查成功")
except Exception as e: except Exception as e:
print(f" {i+1} 创建失败: {e}") logger.error(f"{i+1} 创建失败: {e}")
# 为已存在的表添加缺失的列(静默处理,不显示重复列的提示) # 为已存在的表添加缺失的列(静默处理,不显示重复列的提示)
for table_col in [("model_manage", "purpose"), ("model_eval", "name")]: for table_col in [("model_manage", "purpose"), ("model_eval", "name"), ("fine_tune", "gpus")]:
try: try:
table_name, col_name = table_col table_name, col_name = table_col
cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} VARCHAR(255) DEFAULT ''") cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} JSON")
print(f" {table_name} 表添加 {col_name} 列成功") logger.debug(f"{table_name} 表添加 {col_name} 列成功")
except Exception as e: except Exception:
pass # 列已存在时不输出任何信息 pass # 列已存在时不输出任何信息
# 插入默认管理员用户 # 插入默认管理员用户
cursor.execute("SELECT * FROM users WHERE username = 'admin'") cursor.execute("SELECT * FROM users WHERE username = 'admin'")
if not cursor.fetchone(): if not cursor.fetchone():
cursor.execute("INSERT INTO users (username, password, role) VALUES ('admin', 'admin', 'admin')") cursor.execute("INSERT INTO users (username, password, role) VALUES ('admin', 'admin', 'admin')")
print(" 默认管理员用户创建成功") logger.info("默认管理员用户创建成功")
conn.commit() conn.commit()
cursor.close() cursor.close()
conn.close() conn.close()
print("数据库初始化完成") logger.info("数据库初始化完成")
except Exception as e: except Exception as e:
print(f"数据库初始化失败: {e}") logger.error(f"数据库初始化失败: {e}")
raise raise
@@ -250,6 +330,16 @@ CORS(app, origins="*", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allo
register_blueprints(app) register_blueprints(app)
# ============ 请求日志中间件 ============
@app.after_request
def log_request(response):
"""记录所有API请求"""
# 排除健康检查接口
if request.path != '/api/health':
request_logger.info(f'{request.method} {request.path} - {response.status_code}')
return response
# ============ 健康检查 ============ # ============ 健康检查 ============
@app.route('/api/health', methods=['GET']) @app.route('/api/health', methods=['GET'])
def health_check(): def health_check():
@@ -272,6 +362,7 @@ def health_check():
} }
}) })
except Exception as e: except Exception as e:
logger.error(f"健康检查失败: {e}")
return jsonify({'status': 'error', 'code': 1, 'message': str(e)}) return jsonify({'status': 'error', 'code': 1, 'message': str(e)})
@@ -689,4 +780,9 @@ if __name__ == '__main__':
# 启动前先初始化数据库 # 启动前先初始化数据库
init_database() init_database()
app_config = CONFIG['app'] app_config = CONFIG['app']
app.run(host=app_config['host'], port=app_config['port'], debug=app_config.get('debug', True)) host = app_config['host']
port = app_config['port']
debug = app_config.get('debug', True)
logger.info(f'服务启动于 http://{host}:{port}')
logger.info(f'Debug模式: {debug}')
app.run(host=host, port=port, debug=debug)

View File

@@ -225,6 +225,18 @@
<div class="mb-6"> <div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练配置</h3> <h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练配置</h3>
<!-- GPU硬件选择 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">
<span class="text-red-500 mr-1">*</span>GPU硬件选择
</label>
<div id="gpuSelectionArea" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- GPU列表将通过JS动态加载 -->
<div class="text-sm text-gray-400 col-span-full">正在加载GPU设备...</div>
</div>
<p class="text-xs text-gray-400 mt-2">支持选择多个GPU进行训练多卡训练</p>
</div>
<!-- 训练方式 --> <!-- 训练方式 -->
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方式</label> <label class="block text-sm text-gray-600 mb-3">训练方式</label>
@@ -627,6 +639,9 @@
// 初始化训练方法参数显示 // 初始化训练方法参数显示
toggleTrainMethod(); toggleTrainMethod();
// 加载GPU列表
loadGPUList();
// 设置侧边栏当前页高亮 // 设置侧边栏当前页高亮
const currentPage = 'fine-tune'; const currentPage = 'fine-tune';
document.querySelectorAll('.nav-link').forEach(link => { document.querySelectorAll('.nav-link').forEach(link => {
@@ -790,17 +805,113 @@
} }
} }
// 加载GPU列表
async function loadGPUList() {
const container = document.getElementById('gpuSelectionArea');
if (!container) return;
try {
// 模拟GPU数据 - 实际项目中可以从API获取
const gpuData = await fetchGPUs();
renderGPUList(gpuData);
} catch (error) {
console.error('加载GPU列表失败:', error);
container.innerHTML = '<div class="text-sm text-red-500 col-span-full">加载GPU设备失败请刷新重试</div>';
}
}
// 获取GPU数据模拟数据实际可从API获取
async function fetchGPUs() {
// 实际项目中可以调用后端API获取GPU信息
// const response = await fetch(`${API_BASE}/gpus`);
// return await response.json();
// 模拟GPU数据
return [
{ id: 'gpu0', name: 'NVIDIA A100 80GB', memory: '80GB', cuda_cores: 6912, available: true },
{ id: 'gpu1', name: 'NVIDIA A100 80GB', memory: '80GB', cuda_cores: 6912, available: true },
{ id: 'gpu2', name: 'NVIDIA A100 40GB', memory: '40GB', cuda_cores: 6912, available: true },
{ id: 'gpu3', name: 'NVIDIA A100 40GB', memory: '40GB', cuda_cores: 6912, available: false },
{ id: 'gpu4', name: 'NVIDIA V100 32GB', memory: '32GB', cuda_cores: 5120, available: true },
{ id: 'gpu5', name: 'NVIDIA V100 16GB', memory: '16GB', cuda_cores: 5120, available: false },
{ id: 'gpu6', name: 'NVIDIA RTX 3090', memory: '24GB', cuda_cores: 10496, available: true },
{ id: 'gpu7', name: 'NVIDIA RTX 4090', memory: '24GB', cuda_cores: 16384, available: true }
];
}
// 渲染GPU列表点击卡片选中无需复选框
function renderGPUList(gpus) {
const container = document.getElementById('gpuSelectionArea');
if (!container) return;
container.innerHTML = gpus.map(gpu => `
<div id="gpu_card_${gpu.id}"
class="gpu-card ${!gpu.available ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:border-primary'} border rounded-lg p-3 transition-all"
onclick="toggleGPUSelection('${gpu.id}')"
${!gpu.available ? 'title="该GPU不可用"' : ''}
data-gpu-id="${gpu.id}">
<div class="flex items-center">
<i class="fa fa-microchip text-primary mr-2"></i>
<div class="flex-1">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">${gpu.name}</span>
${gpu.available
? '<i class="fa fa-check-circle text-green-600 text-xs"></i>'
: '<i class="fa fa-times-circle text-red-500 text-xs"></i>'}
</div>
<div class="text-xs text-gray-500 mt-1">
<span class="mr-2"><i class="fa fa-floppy-o mr-1"></i>${gpu.memory}</span>
<span><i class="fa fa-cog mr-1"></i>${gpu.cuda_cores} CUDA</span>
</div>
</div>
</div>
</div>
`).join('');
}
// 切换GPU选择状态
function toggleGPUSelection(gpuId) {
const card = document.getElementById(`gpu_card_${gpuId}`);
if (!card || card.classList.contains('opacity-50')) return;
// 切换选中状态
if (card.classList.contains('border-primary')) {
// 取消选中
card.classList.remove('border-primary', 'bg-blue-50');
card.querySelector('.fa-check-circle').classList.replace('text-primary', 'text-green-600');
} else {
// 选中
card.classList.add('border-primary', 'bg-blue-50');
// 移除检查图标,添加选中标记
const icon = card.querySelector('.fa-check-circle');
if (icon) {
icon.classList.remove('fa-check-circle', 'text-green-600');
icon.classList.add('fa-check', 'text-primary');
}
}
}
// 获取选中的GPU列表
function getSelectedGPUs() {
const cards = document.querySelectorAll('.gpu-card.border-primary');
return Array.from(cards).map(card => card.dataset.gpuId);
}
// 提交表单 // 提交表单
async function submitForm() { async function submitForm() {
const form = document.getElementById('createForm'); const form = document.getElementById('createForm');
const formData = new FormData(form); const formData = new FormData(form);
const validSplit = formData.get('valid_split'); const validSplit = formData.get('valid_split');
// 获取选中的GPU
const selectedGPUs = getSelectedGPUs();
const data = { const data = {
name: formData.get('name'), name: formData.get('name'),
base_model: formData.get('base_model'), base_model: formData.get('base_model'),
train_type: formData.get('train_type'), train_type: formData.get('train_type'),
train_method: formData.get('train_method'), train_method: formData.get('train_method'),
gpus: selectedGPUs, // 添加GPU选择
train_dataset_id: formData.get('train_dataset_id'), train_dataset_id: formData.get('train_dataset_id'),
valid_split: validSplit, valid_split: validSplit,
valid_ratio: parseInt(formData.get('valid_ratio')) || 10, valid_ratio: parseInt(formData.get('valid_ratio')) || 10,
@@ -814,6 +925,10 @@
showMessage('提示', '请输入任务名称', 'warning'); showMessage('提示', '请输入任务名称', 'warning');
return; return;
} }
if (selectedGPUs.length === 0) {
showMessage('提示', '请选择至少一个GPU硬件', 'warning');
return;
}
if (!data.base_model) { if (!data.base_model) {
showMessage('提示', '请选择基础模型', 'warning'); showMessage('提示', '请选择基础模型', 'warning');
return; return;

View File

@@ -236,6 +236,12 @@
<span class="ml-2">平台性能</span> <span class="ml-2">平台性能</span>
</a> </a>
</div> </div>
<div class="nav-item-wrapper">
<a href="#" data-page="logs" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-file-text-o w-5 text-center"></i>
<span class="ml-2">查看日志</span>
</a>
</div>
</nav> </nav>
<!-- 底部信息区域 --> <!-- 底部信息区域 -->
@@ -328,6 +334,58 @@
}; };
const API_BASE = getApiBase(); const API_BASE = getApiBase();
// 日志自动刷新相关变量
let logRefreshTimer = null;
let logCountdownTimer = null;
let logCurrentInterval = 10;
let logFullContent = ''; // 存储完整日志内容
// 设置自动刷新间隔
function setRefreshInterval() {
const select = document.getElementById('logRefreshInterval');
const countdownEl = document.getElementById('logRefreshCountdown');
const secondsEl = document.getElementById('countdownNumber');
if (!select) return;
logCurrentInterval = parseInt(select.value) || 10;
// 清除之前的定时器
if (logRefreshTimer) {
clearInterval(logRefreshTimer);
logRefreshTimer = null;
}
if (logCountdownTimer) {
clearInterval(logCountdownTimer);
logCountdownTimer = null;
}
// 如果选择关闭,不显示倒计时
if (select.value === '0') {
countdownEl.classList.add('hidden');
return;
}
// 显示倒计时
countdownEl.classList.remove('hidden');
secondsEl.textContent = logCurrentInterval;
// 启动倒计时
let countdown = logCurrentInterval;
logCountdownTimer = setInterval(() => {
countdown--;
if (countdown <= 0) {
countdown = logCurrentInterval;
}
secondsEl.textContent = countdown;
}, 1000);
// 启动自动刷新
logRefreshTimer = setInterval(() => {
refreshLogs();
}, logCurrentInterval * 1000);
}
// 获取系统性能监控数据 // 获取系统性能监控数据
async function fetchSystemMetrics() { async function fetchSystemMetrics() {
try { try {
@@ -511,6 +569,12 @@
hasCreate: false, hasCreate: false,
isHardwareMonitor: true isHardwareMonitor: true
}, },
'logs': {
title: '查看日志',
skipFetch: true,
hasCreate: false,
isLogViewer: true
},
'model-compare-chat': { 'model-compare-chat': {
title: '模型对比', title: '模型对比',
skipFetch: true, skipFetch: true,
@@ -660,6 +724,9 @@
// 切换页面时清除选中状态 // 切换页面时清除选中状态
clearSelection(); clearSelection();
// 离开日志页面时停止自动刷新
stopLogAutoRefresh();
const container = document.getElementById('page-content'); const container = document.getElementById('page-content');
const config = tableConfigs[pageName]; const config = tableConfigs[pageName];
@@ -732,6 +799,10 @@
} else if (config.isHardwareMonitor) { } else if (config.isHardwareMonitor) {
// 硬件监控页面使用模拟数据不调用API // 硬件监控页面使用模拟数据不调用API
container.innerHTML = renderConfigPage(config, null); container.innerHTML = renderConfigPage(config, null);
} else if (config.isLogViewer) {
// 日志查看页面
container.innerHTML = renderLogViewerPage(config);
initLogViewer();
} else if (config.isForm) { } else if (config.isForm) {
const data = await fetchData(`${API_BASE}/${config.api}`); const data = await fetchData(`${API_BASE}/${config.api}`);
container.innerHTML = renderConfigPage(config, data); container.innerHTML = renderConfigPage(config, data);
@@ -1106,6 +1177,227 @@
`; `;
} }
// 渲染日志查看页面
function renderLogViewerPage(config) {
return `
<div class="bg-white rounded-lg shadow-sm mb-6">
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<h2 class="text-lg font-medium">${config.title}</h2>
<div class="flex items-center space-x-2">
<button onclick="refreshLogs()" class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary/90 transition-colors">
<i class="fa fa-refresh mr-1"></i>刷新
</button>
</div>
</div>
<div class="p-4">
<!-- 日期和刷新间隔选择 -->
<div class="flex items-center flex-wrap gap-4 mb-4">
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-3">选择日期:</label>
<input type="date" id="logDatePicker" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none" onchange="loadLogFiles()">
</div>
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-3">自动刷新:</label>
<select id="logRefreshInterval" onchange="setRefreshInterval()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="0">关闭</option>
<option value="5">5秒</option>
<option value="10" selected>10秒</option>
<option value="30">30秒</option>
<option value="60">60秒</option>
</select>
</div>
<div id="logRefreshCountdown" class="text-sm text-gray-500 hidden">
<i class="fa fa-clock-o mr-1"></i><span>下次刷新: <span id="countdownNumber">10</span>秒</span>
</div>
</div>
<!-- 日志类型选择 -->
<div class="flex items-center mb-4">
<label class="text-sm text-gray-600 mr-3">日志类型:</label>
<select id="logTypeSelect" onchange="loadSelectedLog()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="">请选择日志文件</option>
</select>
</div>
<!-- 日志内容显示 -->
<div class="border border-gray-200 rounded-lg">
<div class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
<span class="text-sm text-gray-600" id="logFileInfo">请选择日志文件</span>
<div class="flex items-center space-x-3">
<input type="text" id="logSearchInput" placeholder="搜索日志..." oninput="filterLogContent()" class="px-2 py-1 text-xs border border-gray-300 rounded focus:border-primary focus:outline-none">
<span id="logMatchCount" class="text-xs text-gray-500"></span>
<button onclick="clearLogContent()" class="text-xs text-gray-500 hover:text-primary">清空</button>
</div>
</div>
<pre id="logContent" class="p-4 text-xs font-mono bg-gray-900 text-gray-100 overflow-auto max-h-[600px]" style="white-space: pre-wrap; word-wrap: break-word;">日志内容将在这里显示...</pre>
</div>
</div>
</div>
`;
}
// 初始化日志查看器
function initLogViewer() {
const datePicker = document.getElementById('logDatePicker');
if (datePicker) {
const today = new Date().toISOString().split('T')[0];
datePicker.value = today;
loadLogFiles();
}
// 启动自动刷新
setRefreshInterval();
}
// 加载日志文件列表
async function loadLogFiles() {
const datePicker = document.getElementById('logDatePicker');
const logTypeSelect = document.getElementById('logTypeSelect');
const selectedDate = datePicker.value;
logTypeSelect.innerHTML = '<option value="">加载中...</option>';
try {
const response = await fetch(`${API_BASE}/log-files?date=${selectedDate}`);
const result = await response.json();
if (result.code === 0 && result.data) {
logTypeSelect.innerHTML = '<option value="">请选择日志文件</option>';
result.data.forEach(log => {
const option = document.createElement('option');
option.value = log.file;
option.textContent = log.name + ' (' + log.size + ')';
logTypeSelect.appendChild(option);
});
// 如果有日志文件,自动加载第一个
if (result.data.length > 0) {
logTypeSelect.value = result.data[0].file;
loadSelectedLog();
}
} else {
logTypeSelect.innerHTML = '<option value="">暂无日志文件</option>';
document.getElementById('logContent').textContent = '该日期暂无日志文件';
document.getElementById('logFileInfo').textContent = '无日志文件';
}
} catch (error) {
console.error('加载日志文件列表失败:', error);
logTypeSelect.innerHTML = '<option value="">加载失败</option>';
document.getElementById('logContent').textContent = '加载日志文件列表失败: ' + error.message;
}
}
// 加载选中的日志
async function loadSelectedLog() {
const logTypeSelect = document.getElementById('logTypeSelect');
const logFile = logTypeSelect.value;
const logContent = document.getElementById('logContent');
const logFileInfo = document.getElementById('logFileInfo');
if (!logFile) {
logContent.textContent = '请选择日志文件';
logFileInfo.textContent = '无日志文件';
return;
}
logContent.textContent = '加载中...';
logFileInfo.textContent = '加载中...';
try {
const response = await fetch(`${API_BASE}/log-content?file=${encodeURIComponent(logFile)}`);
const result = await response.json();
if (result.code === 0) {
logFullContent = result.data.content || '';
logContent.textContent = logFullContent || '(空日志)';
logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')';
// 清空搜索框和匹配计数
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
// 滚动到最底部
scrollToLogBottom();
} else {
logContent.textContent = '加载失败: ' + (result.message || '未知错误');
logFileInfo.textContent = '加载失败';
}
} catch (error) {
console.error('加载日志内容失败:', error);
logContent.textContent = '加载日志内容失败: ' + error.message;
logFileInfo.textContent = '加载失败';
}
}
// 刷新日志
function refreshLogs() {
loadLogFiles();
if (document.getElementById('logTypeSelect').value) {
loadSelectedLog();
}
// 重置倒计时
const select = document.getElementById('logRefreshInterval');
const secondsEl = document.getElementById('countdownNumber');
if (select && select.value !== '0' && secondsEl) {
secondsEl.textContent = logCurrentInterval;
}
}
// 停止日志自动刷新(离开页面时调用)
function stopLogAutoRefresh() {
if (logRefreshTimer) {
clearInterval(logRefreshTimer);
logRefreshTimer = null;
}
if (logCountdownTimer) {
clearInterval(logCountdownTimer);
logCountdownTimer = null;
}
}
// 滚动到日志底部
function scrollToLogBottom() {
const logContent = document.getElementById('logContent');
if (logContent) {
logContent.scrollTop = logContent.scrollHeight;
}
}
// 过滤日志内容
function filterLogContent() {
const searchInput = document.getElementById('logSearchInput');
const matchCount = document.getElementById('logMatchCount');
const logContent = document.getElementById('logContent');
if (!searchInput || !matchCount || !logContent) return;
const keyword = searchInput.value.trim();
if (!keyword) {
logContent.textContent = logFullContent || '(空日志)';
matchCount.textContent = '';
scrollToLogBottom();
return;
}
const lines = logFullContent.split('\n');
const matchingLines = lines.filter(line => line.toLowerCase().includes(keyword.toLowerCase()));
if (matchingLines.length > 0) {
logContent.textContent = matchingLines.join('\n');
matchCount.textContent = `(${matchingLines.length}条匹配)`;
// 滚动到最底部查看最新匹配
scrollToLogBottom();
} else {
logContent.textContent = '未找到匹配的日志';
matchCount.textContent = '(0条匹配)';
}
}
// 清空日志内容显示
function clearLogContent() {
document.getElementById('logContent').textContent = '日志内容将在这里显示...';
document.getElementById('logFileInfo').textContent = '请选择日志文件';
document.getElementById('logTypeSelect').value = '';
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
logFullContent = '';
}
// 渲染工具卡片页面 // 渲染工具卡片页面
function renderToolsPage(config) { function renderToolsPage(config) {
// 渲染单个工具卡片 // 渲染单个工具卡片
@@ -2511,6 +2803,64 @@
} }
} }
} }
// ============ Web日志系统 ============
const webLogger = {
_currentPage: 'main',
// 初始化当前页面名称
init: function(pageName) {
this._currentPage = pageName || 'unknown';
},
// 发送日志到服务器
_sendLog: async function(level, message) {
try {
await fetch(`${API_BASE}/web-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: level,
message: message,
page: this._currentPage,
timestamp: new Date().toISOString()
})
});
} catch (e) {
// 发送失败时只记录到控制台
console.warn('日志发送失败:', e);
}
},
info: function(message) {
console.log(`[INFO] ${message}`);
this._sendLog('info', message);
},
error: function(message) {
console.error(`[ERROR] ${message}`);
this._sendLog('error', message);
},
warning: function(message) {
console.warn(`[WARNING] ${message}`);
this._sendLog('warning', message);
},
debug: function(message) {
console.debug(`[DEBUG] ${message}`);
this._sendLog('debug', message);
}
};
// 页面加载完成后初始化日志
document.addEventListener('DOMContentLoaded', function() {
// 获取当前页面名称
const path = window.location.pathname;
const pageName = path.split('/').pop().replace('.html', '') || 'main';
webLogger.init(pageName);
webLogger.info('页面加载完成');
});
</script> </script>
<!-- 自定义消息弹窗 --> <!-- 自定义消息弹窗 -->

View File

@@ -94,6 +94,23 @@
</div> </div>
</div> </div>
<!-- 自定义确认弹窗 -->
<div id="confirmModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden">
<div class="p-6 text-center">
<div id="confirmIcon" class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center">
<i class="fa fa-exclamation text-xl text-yellow-600"></i>
</div>
<h3 id="confirmTitle" class="text-lg font-medium text-gray-800 mb-2"></h3>
<p id="confirmMessage" class="text-gray-600 text-sm"></p>
</div>
<div class="px-6 pb-6 flex justify-center space-x-4">
<button id="confirmCancelBtn" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors">取消</button>
<button id="confirmOkBtn" class="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">确定</button>
</div>
</div>
</div>
<script> <script>
// 动态获取 API 基础地址 // 动态获取 API 基础地址
const getApiBase = () => { const getApiBase = () => {
@@ -263,17 +280,112 @@
return 'bg-gray-50 text-gray-500'; return 'bg-gray-50 text-gray-500';
} }
// 自定义确认弹窗
function showConfirm(title, message, onConfirm) {
const modal = document.getElementById('confirmModal');
const modalTitle = document.getElementById('confirmTitle');
const modalMessage = document.getElementById('confirmMessage');
const cancelBtn = document.getElementById('confirmCancelBtn');
const okBtn = document.getElementById('confirmOkBtn');
modalTitle.textContent = title;
modalMessage.textContent = message;
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
cancelBtn.onclick = function() {
modal.classList.add('hidden');
document.body.style.overflow = '';
};
okBtn.onclick = function() {
modal.classList.add('hidden');
document.body.style.overflow = '';
if (typeof onConfirm === 'function') {
onConfirm();
}
};
}
// 自定义消息弹窗
function showMessage(title, message, type = 'info', onConfirm) {
const modal = document.getElementById('confirmModal');
const modalTitle = document.getElementById('confirmTitle');
const modalMessage = document.getElementById('confirmMessage');
const cancelBtn = document.getElementById('confirmCancelBtn');
const okBtn = document.getElementById('confirmOkBtn');
// 隐藏取消按钮,只显示确定按钮
cancelBtn.classList.add('hidden');
modalTitle.textContent = title;
modalMessage.innerHTML = message;
// 根据类型设置图标和按钮颜色
const iconContainer = document.getElementById('confirmIcon');
if (type === 'success') {
iconContainer.className = 'w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center';
iconContainer.innerHTML = '<i class="fa fa-check text-xl text-green-600"></i>';
okBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
} else if (type === 'error') {
iconContainer.className = 'w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center';
iconContainer.innerHTML = '<i class="fa fa-times text-xl text-red-600"></i>';
okBtn.className = 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors';
} else if (type === 'warning') {
iconContainer.className = 'w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center';
iconContainer.innerHTML = '<i class="fa fa-exclamation text-xl text-yellow-600"></i>';
okBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
} else {
iconContainer.className = 'w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center';
iconContainer.innerHTML = '<i class="fa fa-info text-xl text-blue-600"></i>';
okBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
}
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
okBtn.onclick = function() {
modal.classList.add('hidden');
document.body.style.overflow = '';
// 恢复取消按钮
cancelBtn.classList.remove('hidden');
if (typeof onConfirm === 'function') {
onConfirm();
}
};
}
// 操作函数(挂载到 window 以便 onclick 调用) // 操作函数(挂载到 window 以便 onclick 调用)
window.viewReport = function(id) { window.viewReport = function(id) {
alert('查看报告功能开发中'); alert('查看报告功能开发中');
}; };
window.deleteTask = function(id) { window.deleteTask = function(id) {
if (confirm('确定要删除此评测任务吗?')) { showConfirm('确认删除', '确定要删除此评测任务吗?', () => {
alert('删除功能开发中'); executeDeleteTask(id);
} });
}; };
async function executeDeleteTask(id) {
try {
const response = await fetch(`${API_BASE}/model-eval/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
showMessage('成功', '删除成功', 'success', () => {
loadTasks();
});
} else {
showMessage('错误', result.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除评测任务失败:', error);
showMessage('错误', '删除失败: ' + error.message, 'error');
}
}
window.addDimension = function() { window.addDimension = function() {
window.location.href = 'model-dimension-create.html'; window.location.href = 'model-dimension-create.html';
}; };