文件上传页面功能基本集成完成

This commit is contained in:
2026-01-19 17:28:58 +08:00
parent 88eaa33db0
commit bfaeb24d9e
10 changed files with 16758 additions and 248 deletions

View File

@@ -33,7 +33,8 @@
"Bash(./test_upload.sh:*)", "Bash(./test_upload.sh:*)",
"Bash(./test_all.sh)", "Bash(./test_all.sh)",
"Bash(/data/code/FT_Platform/YG_FT_Platform/test_data_dir.sh:*)", "Bash(/data/code/FT_Platform/YG_FT_Platform/test_data_dir.sh:*)",
"Bash(grep:*)" "Bash(grep:*)",
"Bash(mysql:*)"
] ]
} }
} }

File diff suppressed because one or more lines are too long

9
src/api/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
"""
API 路由包
"""
from .datasets import datasets_bp
# 注册所有蓝图
def register_blueprints(app):
"""注册所有蓝图"""
app.register_blueprint(datasets_bp)

430
src/api/datasets.py Normal file
View File

@@ -0,0 +1,430 @@
"""
数据集管理 API 路由
"""
import io
import os
import time
import zipfile
from flask import Blueprint, request, jsonify, send_from_directory, Response
from werkzeug.utils import secure_filename
# 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DATASET_FOLDER = os.path.join(PROJECT_ROOT, 'datasets')
ALLOWED_EXTENSIONS = {'jsonl', 'json', 'xls', 'xlsx'}
# 创建蓝图
datasets_bp = Blueprint('datasets', __name__, url_prefix='/api/dataset-manage')
def get_db_connection():
"""获取数据库连接"""
import pymysql
import yaml
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
CONFIG = yaml.safe_load(f)
db_config = CONFIG['database']
return pymysql.connect(
host=db_config['host'],
port=db_config['port'],
user=db_config['username'],
password=db_config['password'],
database=db_config['name'],
charset=db_config.get('charset', 'utf8mb4'),
cursorclass=pymysql.cursors.DictCursor
)
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"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.1f} MB"
else:
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def generic_get_by_id(table_name, id_val):
"""通用按ID查询"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (id_val,))
result = cursor.fetchone()
cursor.close()
conn.close()
return result
# ============ 数据集管理 CRUD ============
@datasets_bp.route('/<int:id>', methods=['GET'])
def get_dataset(id):
"""获取单个数据集详情"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM dataset_manage WHERE id = %s", (id,))
dataset = cursor.fetchone()
if not dataset:
cursor.close()
conn.close()
return jsonify({'code': 1, 'message': '数据集不存在'})
# 获取关联的文件列表
cursor.execute(
"SELECT id, file_name, file_path, file_size, file_type, create_time FROM dataset_files WHERE dataset_id = %s ORDER BY create_time DESC",
(id,)
)
files = cursor.fetchall()
# 格式化文件大小
for f in files:
f['file_size_formatted'] = format_file_size(f['file_size'])
dataset['files'] = files
cursor.close()
conn.close()
return jsonify({'code': 0, 'data': dataset})
@datasets_bp.route('', methods=['GET'])
def get_datasets():
"""获取所有数据集"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM dataset_manage ORDER BY create_time DESC")
result = cursor.fetchall()
cursor.close()
conn.close()
return jsonify({'code': 0, 'data': result})
@datasets_bp.route('', methods=['POST'])
def create_dataset():
"""创建数据集"""
data = request.json
try:
conn = get_db_connection()
cursor = conn.cursor()
columns = ', '.join(data.keys())
placeholders = ', '.join(['%s'] * len(data))
sql = f"INSERT INTO dataset_manage ({columns}) VALUES ({placeholders})"
cursor.execute(sql, list(data.values()))
conn.commit()
new_id = cursor.lastrowid
cursor.close()
conn.close()
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
except Exception as e:
return jsonify({'code': 1, 'message': f'创建失败: {str(e)}'})
@datasets_bp.route('/<int:id>', methods=['PUT'])
def update_dataset(id):
"""更新数据集"""
data = request.json
conn = get_db_connection()
cursor = conn.cursor()
set_clause = ', '.join([f"{k} = %s" for k in data.keys()])
sql = f"UPDATE dataset_manage SET {set_clause} WHERE id = %s"
values = list(data.values()) + [id]
cursor.execute(sql, values)
conn.commit()
cursor.close()
conn.close()
return jsonify({'code': 0, 'message': '更新成功'})
@datasets_bp.route('/<int:id>', methods=['DELETE'])
def delete_dataset(id):
"""删除数据集"""
conn = get_db_connection()
cursor = conn.cursor()
# 获取文件路径列表
cursor.execute("SELECT file_path FROM dataset_files WHERE dataset_id = %s", (id,))
files = cursor.fetchall()
# 删除文件
for f in files:
file_path = f.get('file_path')
if file_path and os.path.exists(file_path):
try:
os.remove(file_path)
except Exception as e:
print(f"删除文件失败: {file_path}, {e}")
# 删除数据库记录
cursor.execute("DELETE FROM dataset_files WHERE dataset_id = %s", (id,))
cursor.execute("DELETE FROM dataset_manage WHERE id = %s", (id,))
conn.commit()
cursor.close()
conn.close()
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 数据集文件上传接口 ============
@datasets_bp.route('/upload/<int:dataset_id>', methods=['POST'])
def upload_dataset_file(dataset_id):
"""上传数据集文件"""
# 检查数据集是否存在
dataset = generic_get_by_id('dataset_manage', dataset_id)
if not dataset:
return jsonify({'code': 1, 'message': '数据集不存在'})
# 确保上传目录存在datasets根目录
os.makedirs(DATASET_FOLDER, exist_ok=True)
uploaded_files = []
errors = []
if 'files' not in request.files:
return jsonify({'code': 1, 'message': '没有文件被上传'})
files = request.files.getlist('files')
for file in files:
if file.filename == '':
continue
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# 添加时间戳和dataset_id防止文件名冲突格式timestamp_datasetId_filename
timestamp = int(time.time() * 1000)
new_filename = f"{timestamp}_{dataset_id}_{filename}"
file_path = os.path.join(DATASET_FOLDER, new_filename)
# 保存文件
file.save(file_path)
file_size = os.path.getsize(file_path)
# 获取文件扩展名
ext = filename.rsplit('.', 1)[1].lower()
# 保存文件信息到数据库
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO dataset_files (dataset_id, file_name, file_path, file_size, file_type) VALUES (%s, %s, %s, %s, %s)",
(dataset_id, filename, file_path, file_size, ext)
)
conn.commit()
cursor.close()
conn.close()
uploaded_files.append({
'name': filename,
'size': file_size,
'size_formatted': format_file_size(file_size)
})
else:
errors.append(f"{file.filename}: 文件类型不支持")
# 如果有成功上传的文件,才更新数据集的文件数量和大小
if uploaded_files:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) as count, SUM(file_size) as total_size FROM dataset_files WHERE dataset_id = %s", (dataset_id,))
result = cursor.fetchone()
file_count = result['count'] or 0
total_size = result['total_size'] or 0
cursor.execute(
"UPDATE dataset_manage SET file_count = %s, size = %s WHERE id = %s",
(file_count, format_file_size(total_size), dataset_id)
)
conn.commit()
cursor.close()
conn.close()
if errors:
return jsonify({
'code': 0,
'message': f'部分文件上传成功,{len(errors)}个文件失败',
'data': {
'uploaded': uploaded_files,
'errors': errors
}
})
return jsonify({
'code': 0,
'message': f'成功上传 {len(uploaded_files)} 个文件',
'data': {
'uploaded': uploaded_files,
'file_count': file_count
}
})
@datasets_bp.route('/<int:dataset_id>/files', methods=['GET'])
def get_dataset_files(dataset_id):
"""获取数据集文件列表"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"SELECT id, file_name, file_path, file_size, file_type, create_time FROM dataset_files WHERE dataset_id = %s ORDER BY create_time DESC",
(dataset_id,)
)
files = cursor.fetchall()
cursor.close()
conn.close()
# 格式化文件大小
for f in files:
f['file_size_formatted'] = format_file_size(f['file_size'])
return jsonify({'code': 0, 'data': files})
@datasets_bp.route('/files/<int:file_id>', methods=['DELETE'])
def delete_dataset_file(file_id):
"""删除数据集文件"""
conn = get_db_connection()
cursor = conn.cursor()
# 获取文件信息
cursor.execute("SELECT dataset_id, file_path FROM dataset_files WHERE id = %s", (file_id,))
file_info = cursor.fetchone()
if not file_info:
cursor.close()
conn.close()
return jsonify({'code': 1, 'message': '文件不存在'})
# 删除物理文件
file_path = file_info['file_path']
if file_path and os.path.exists(file_path):
try:
os.remove(file_path)
except Exception as e:
print(f"删除文件失败: {file_path}, {e}")
# 删除数据库记录
cursor.execute("DELETE FROM dataset_files WHERE id = %s", (file_id,))
# 更新数据集的文件数量和大小
dataset_id = file_info['dataset_id']
cursor.execute("SELECT COUNT(*) as count, SUM(file_size) as total_size FROM dataset_files WHERE dataset_id = %s", (dataset_id,))
result = cursor.fetchone()
file_count = result['count'] or 0
total_size = result['total_size'] or 0
cursor.execute(
"UPDATE dataset_manage SET file_count = %s, size = %s WHERE id = %s",
(file_count, format_file_size(total_size), dataset_id)
)
conn.commit()
cursor.close()
conn.close()
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 文件下载接口 ============
@datasets_bp.route('/download/<int:dataset_id>/<filename>', methods=['GET'])
def download_dataset_file(dataset_id, filename):
"""下载数据集文件"""
# 文件直接存储在 DATASET_FOLDER 根目录下
return send_from_directory(DATASET_FOLDER, filename, as_attachment=True)
@datasets_bp.route('/download/<int:dataset_id>', methods=['GET'])
def download_dataset_all(dataset_id):
"""下载数据集所有文件ZIP打包"""
conn = get_db_connection()
cursor = conn.cursor()
# 获取数据集信息
cursor.execute("SELECT name FROM dataset_manage WHERE id = %s", (dataset_id,))
dataset = cursor.fetchone()
if not dataset:
cursor.close()
conn.close()
return jsonify({'code': 1, 'message': '数据集不存在'})
# 获取所有文件
cursor.execute(
"SELECT id, file_name, file_path FROM dataset_files WHERE dataset_id = %s ORDER BY create_time DESC",
(dataset_id,)
)
files = cursor.fetchall()
cursor.close()
conn.close()
if not files:
return jsonify({'code': 1, 'message': '数据集没有文件'})
# 创建ZIP文件
memory_file = io.BytesIO()
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf:
for f in files:
file_path = f.get('file_path')
if file_path and os.path.exists(file_path):
# 使用原始文件名
zf.write(file_path, f['file_name'])
memory_file.seek(0)
# 发送ZIP文件
zip_name = f"{dataset['name'] or 'dataset'}_{dataset_id}.zip"
return Response(
memory_file,
mimetype='application/zip',
headers={'Content-Disposition': f'attachment;filename={zip_name}'}
)
# ============ 文件预览接口 ============
@datasets_bp.route('/preview/<int:file_id>', methods=['GET'])
def preview_dataset_file(file_id):
"""预览数据集文件内容限100KB"""
conn = get_db_connection()
cursor = conn.cursor()
# 获取文件信息
cursor.execute("SELECT id, file_name, file_path, file_type FROM dataset_files WHERE id = %s", (file_id,))
file_info = cursor.fetchone()
if not file_info:
cursor.close()
conn.close()
return jsonify({'code': 1, 'message': '文件不存在'})
file_path = file_info['file_path']
if not file_path or not os.path.exists(file_path):
cursor.close()
conn.close()
return jsonify({'code': 1, 'message': '文件不存在'})
# 读取文件内容限100KB
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read(102400) # 100KB
except Exception as e:
cursor.close()
conn.close()
return jsonify({'code': 1, 'message': f'读取文件失败: {str(e)}'})
cursor.close()
conn.close()
return jsonify({
'code': 0,
'data': {
'file_name': file_info['file_name'],
'content': content
}
})

View File

@@ -6,8 +6,13 @@ import sys
import json import json
import pymysql import pymysql
import yaml import yaml
from flask import Flask, request, jsonify import time
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
# 导入API蓝图
from api import register_blueprints
# 获取项目根目录 # 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -42,6 +47,8 @@ def get_db_connection():
def init_database(): def init_database():
"""初始化数据库表""" """初始化数据库表"""
print("正在初始化数据库...")
try:
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
@@ -61,7 +68,7 @@ def init_database():
progress INT DEFAULT 0, progress INT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 我的模型表 # 我的模型表
"""CREATE TABLE IF NOT EXISTS my_models ( """CREATE TABLE IF NOT EXISTS my_models (
@@ -72,7 +79,7 @@ def init_database():
description TEXT, description TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 模型评测表 # 模型评测表
"""CREATE TABLE IF NOT EXISTS model_eval ( """CREATE TABLE IF NOT EXISTS model_eval (
@@ -83,7 +90,7 @@ def init_database():
score DECIMAL(10, 4), score DECIMAL(10, 4),
status VARCHAR(50) DEFAULT 'completed', status VARCHAR(50) DEFAULT 'completed',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP create_time DATETIME DEFAULT CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 模型部署表 # 模型部署表
"""CREATE TABLE IF NOT EXISTS model_deploy ( """CREATE TABLE IF NOT EXISTS model_deploy (
@@ -94,7 +101,7 @@ def init_database():
status VARCHAR(50) DEFAULT 'running', status VARCHAR(50) DEFAULT 'running',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 数据集管理表 # 数据集管理表
"""CREATE TABLE IF NOT EXISTS dataset_manage ( """CREATE TABLE IF NOT EXISTS dataset_manage (
@@ -104,9 +111,25 @@ def init_database():
size VARCHAR(50), size VARCHAR(50),
count INT, count INT,
description TEXT, description TEXT,
file_path VARCHAR(500),
file_count INT DEFAULT 0,
storage_type VARCHAR(50) DEFAULT 'local',
minio_config TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 数据集文件表
"""CREATE TABLE IF NOT EXISTS dataset_files (
id INT AUTO_INCREMENT PRIMARY KEY,
dataset_id INT NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT DEFAULT 0,
file_type VARCHAR(50),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (dataset_id) REFERENCES dataset_manage(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 数据生成表 # 数据生成表
"""CREATE TABLE IF NOT EXISTS data_generate ( """CREATE TABLE IF NOT EXISTS data_generate (
@@ -117,7 +140,7 @@ def init_database():
status VARCHAR(50) DEFAULT 'pending', status VARCHAR(50) DEFAULT 'pending',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 权限管理表 # 权限管理表
"""CREATE TABLE IF NOT EXISTS permission ( """CREATE TABLE IF NOT EXISTS permission (
@@ -127,7 +150,7 @@ def init_database():
permissions TEXT, permissions TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 模型管理表 # 模型管理表
"""CREATE TABLE IF NOT EXISTS model_manage ( """CREATE TABLE IF NOT EXISTS model_manage (
@@ -139,7 +162,7 @@ def init_database():
description TEXT, description TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 系统配置表 # 系统配置表
"""CREATE TABLE IF NOT EXISTS sys_config ( """CREATE TABLE IF NOT EXISTS sys_config (
@@ -148,7 +171,7 @@ def init_database():
config_value TEXT, config_value TEXT,
description VARCHAR(255), description VARCHAR(255),
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 用户表 # 用户表
"""CREATE TABLE IF NOT EXISTS users ( """CREATE TABLE IF NOT EXISTS users (
@@ -157,27 +180,38 @@ def init_database():
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user', role VARCHAR(50) DEFAULT 'user',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP create_time DATETIME DEFAULT CURRENT_TIMESTAMP
)""" ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"""
] ]
for table_sql in tables: for i, table_sql in enumerate(tables):
try:
cursor.execute(table_sql) cursor.execute(table_sql)
print(f"{i+1}/{len(tables)} 创建/检查成功")
except Exception as e:
print(f"{i+1} 创建失败: {e}")
# 插入默认管理员用户 # 插入默认管理员用户
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(" 默认管理员用户创建成功")
conn.commit() conn.commit()
cursor.close() cursor.close()
conn.close() conn.close()
print("数据库初始化完成") print("数据库初始化完成")
except Exception as e:
print(f"数据库初始化失败: {e}")
raise
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = CONFIG['secret_key'] app.config['SECRET_KEY'] = CONFIG['secret_key']
CORS(app, resources={r"/api/*": {"origins": "*"}}) CORS(app, resources={r"/api/*": {"origins": "*"}})
# 注册蓝图
register_blueprints(app)
# ============ 健康检查 ============ # ============ 健康检查 ============
@app.route('/api/health', methods=['GET']) @app.route('/api/health', methods=['GET'])
@@ -370,32 +404,6 @@ def delete_model_deploy(id):
return jsonify({'code': 0, 'message': '删除成功'}) return jsonify({'code': 0, 'message': '删除成功'})
# ============ 数据集管理接口 ============
@app.route('/api/dataset-manage', methods=['GET'])
def get_dataset_manage():
return jsonify({'code': 0, 'data': generic_get_all('dataset_manage')})
@app.route('/api/dataset-manage', methods=['POST'])
def create_dataset_manage():
data = request.json
new_id = generic_create('dataset_manage', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/dataset-manage/<int:id>', methods=['PUT'])
def update_dataset_manage(id):
data = request.json
generic_update('dataset_manage', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/dataset-manage/<int:id>', methods=['DELETE'])
def delete_dataset_manage(id):
generic_delete('dataset_manage', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 数据生成接口 ============ # ============ 数据生成接口 ============
@app.route('/api/data-generate', methods=['GET']) @app.route('/api/data-generate', methods=['GET'])
def get_data_generate(): def get_data_generate():
@@ -501,6 +509,7 @@ def delete_sys_config(id):
if __name__ == '__main__': 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)) app.run(host=app_config['host'], port=app_config['port'], debug=app_config.get('debug', True))

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传数据集 / 远光软件微调平台</title> <title>上传数据集 - 远光软件微调平台</title>
<script src="../lib/tailwindcss/tailwind.js"></script> <script src="../lib/tailwindcss/tailwind.js"></script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet"> <link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<style> <style>
@@ -161,7 +161,7 @@
<div class="flex items-center text-sm"> <div class="flex items-center text-sm">
<span id="breadcrumbParent" class="text-primary cursor-pointer hover:underline" onclick="goBack()">数据集管理</span> <span id="breadcrumbParent" class="text-primary cursor-pointer hover:underline" onclick="goBack()">数据集管理</span>
<span class="mx-2 text-gray-300">/</span> <span class="mx-2 text-gray-300">/</span>
<span class="text-gray-800 font-medium">上传数据集</span> <span id="breadcrumbChild" class="text-gray-800 font-medium">上传数据集</span>
</div> </div>
</div> </div>
@@ -186,6 +186,23 @@
</div> </div>
</div> </div>
<!-- 1.1 数据集描述输入框 -->
<div class="mb-6">
<label class="form-label">
数据集描述
</label>
<div class="relative">
<textarea
name="description"
placeholder="请输入数据集描述(选填)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none resize-none"
rows="3"
maxlength="50"
></textarea>
<span class="absolute right-3 bottom-2 text-gray-400 text-sm">0 / 50</span>
</div>
</div>
<!-- 2. 数据集类型(单选按钮) --> <!-- 2. 数据集类型(单选按钮) -->
<div class="mb-6 pl-4"> <div class="mb-6 pl-4">
<label class="block text-sm font-medium text-gray-700 mb-2">数据集类型</label> <label class="block text-sm font-medium text-gray-700 mb-2">数据集类型</label>
@@ -278,16 +295,29 @@
<!-- 5. 上传文件区域 --> <!-- 5. 上传文件区域 -->
<div class="mb-6 pl-4"> <div class="mb-6 pl-4">
<label class="block text-sm font-medium text-gray-700 mb-1">上传文件</label> <label class="block text-sm font-medium text-gray-700 mb-1">上传文件</label>
<p class="text-xs text-gray-500 mb-2">选择文件进行上传,数据格式可下载模板查看一次最多导入10个文件</p> <p class="text-xs text-gray-500 mb-2">选择文件进行上传,数据格式可下载模板查看</p>
<div <div
id="upload-area" id="upload-area"
class="upload-area border-2 border-dashed border-gray-300 rounded-lg p-8 text-center transition-colors cursor-pointer relative" class="upload-area border-2 border-dashed border-gray-300 rounded-lg p-8 text-center transition-colors cursor-pointer relative"
> >
<input type="file" id="file-upload" class="absolute opacity-0" multiple accept=".jsonl,.xls,.xlsx"> <input type="file" id="file-upload" class="absolute opacity-0" accept=".jsonl,.json">
<div class="flex flex-col items-center space-y-2"> <div class="flex flex-col items-center space-y-2">
<i class="fa fa-cloud-upload text-2xl text-gray-400"></i> <i class="fa fa-cloud-upload text-2xl text-gray-400"></i>
<p class="text-sm text-gray-600">点击或将文件拖拽到这里上传 (<span id="fileCount">0</span>/10)</p> <p class="text-sm text-gray-600">点击或将文件拖拽到这里上传</p>
<p class="text-xs text-gray-500">支持扩展名jsonl, xls, xlsx, 文件最大200MB<br>一次最多导入10个文件</p> <p class="text-xs text-gray-500">支持扩展名jsonl, json文件最大200MB</p>
</div>
</div>
<!-- 数据统计信息 -->
<div id="dataStats" class="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200 hidden">
<div class="flex items-center space-x-4">
<div id="formatCheck" class="flex items-center hidden">
<i class="fa fa-check-circle text-green-500 mr-1"></i>
<span class="text-sm text-green-600">符合 Alpaca 格式</span>
</div>
<div id="formatError" class="flex items-center hidden">
<i class="fa fa-exclamation-circle text-red-500 mr-1"></i>
<span class="text-sm text-red-600">格式异常</span>
</div>
</div> </div>
</div> </div>
<!-- 已上传文件列表 --> <!-- 已上传文件列表 -->
@@ -296,9 +326,6 @@
<!-- 8. 模板链接 --> <!-- 8. 模板链接 -->
<div class="mb-6 pl-4 space-x-4"> <div class="mb-6 pl-4 space-x-4">
<a href="#" class="text-primary text-sm hover:underline">
<i class="fa fa-file-excel mr-1"></i>EXCEL数据模板
</a>
<a href="#" class="text-primary text-sm hover:underline"> <a href="#" class="text-primary text-sm hover:underline">
<i class="fa fa-file-code mr-1"></i>JSON数据模板 <i class="fa fa-file-code mr-1"></i>JSON数据模板
</a> </a>
@@ -336,18 +363,36 @@
// 已选择的文件列表 // 已选择的文件列表
let selectedFiles = []; let selectedFiles = [];
// 编辑模式
let editId = null;
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', async function() {
// 根据URL参数设置返回页面 // 根据URL参数设置返回页面
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const from = urlParams.get('from'); const from = urlParams.get('from');
const id = urlParams.get('id');
const breadcrumbParent = document.getElementById('breadcrumbParent'); const breadcrumbParent = document.getElementById('breadcrumbParent');
const breadcrumbChild = document.getElementById('breadcrumbChild');
if (from === 'fine-tune') { if (from === 'fine-tune') {
backUrl = 'fine-tune-create.html'; backUrl = 'fine-tune-create.html';
if (breadcrumbParent) { if (breadcrumbParent) {
breadcrumbParent.textContent = '创建训练任务'; breadcrumbParent.textContent = '创建训练任务';
} }
} }
// 检查是否是编辑模式
if (id) {
editId = parseInt(id);
document.title = '编辑数据集 - 远光软件微调平台';
if (breadcrumbChild) {
breadcrumbChild.textContent = '编辑数据集';
}
// 加载现有数据
await loadDatasetData(editId);
}
// 文件上传区域拖拽逻辑 // 文件上传区域拖拽逻辑
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
const fileUpload = document.getElementById('file-upload'); const fileUpload = document.getElementById('file-upload');
@@ -379,10 +424,16 @@
handleFiles(files); handleFiles(files);
}); });
// 监听文件选择 // 监听文件选择(每次只上传一个文件,新选择会替换旧文件)
fileUpload.addEventListener('change', () => { fileUpload.addEventListener('change', () => {
const files = Array.from(fileUpload.files); const files = Array.from(fileUpload.files);
handleFiles(files); if (files.length > 0) {
// 清空之前的文件,只保留新选择的第一个文件
selectedFiles = [];
handleFiles([files[0]]);
}
// 清空文件输入框
fileUpload.value = '';
}); });
// 绑定导航点击事件 // 绑定导航点击事件
@@ -399,6 +450,86 @@
initRadioStyles(); initRadioStyles();
}); });
// 加载数据集数据(编辑模式)
async function loadDatasetData(id) {
try {
const response = await fetch(`${API_BASE}/dataset-manage/${id}`);
const result = await response.json();
if (result.code !== 0) {
showMessage('错误', result.message || '获取数据集信息失败', 'error');
return;
}
const data = result.data;
if (!data) {
showMessage('错误', '数据集不存在', 'error');
return;
}
// 填充表单
const form = document.getElementById('datasetForm');
// 数据集名称
const nameInput = form.querySelector('input[name="name"]');
if (nameInput) nameInput.value = data.name || '';
// 数据集描述
const descInput = form.querySelector('textarea[name="description"]');
if (descInput) descInput.value = data.description || '';
// 数据集类型
const typeValue = data.type || 'train';
const typeRadio = form.querySelector(`input[name="dataset_type"][value="${typeValue}"]`);
if (typeRadio) {
typeRadio.checked = true;
initRadioStyles();
}
// 存储位置
const storageValue = data.storage_type || 'local';
const storageRadio = form.querySelector(`input[name="storage"][value="${storageValue}"]`);
if (storageRadio) {
storageRadio.checked = true;
initRadioStyles();
toggleStorageConfig();
// 如果是MinIO填充配置
if (storageValue === 'minio' && data.minio_config) {
const config = typeof data.minio_config === 'string' ? JSON.parse(data.minio_config) : data.minio_config;
const endpointInput = form.querySelector('input[name="minio_endpoint"]');
const bucketInput = form.querySelector('input[name="minio_bucket"]');
const accessKeyInput = form.querySelector('input[name="minio_access_key"]');
const secretKeyInput = form.querySelector('input[name="minio_secret_key"]');
const sslCheckbox = form.querySelector('input[name="minio_ssl"]');
if (endpointInput) endpointInput.value = config.endpoint || '';
if (bucketInput) bucketInput.value = config.bucket || '';
if (accessKeyInput) accessKeyInput.value = config.access_key || '';
if (secretKeyInput) secretKeyInput.value = config.secret_key || '';
if (sslCheckbox) sslCheckbox.checked = config.ssl || false;
}
}
// 加载已上传的文件列表
if (data.files && data.files.length > 0) {
// 将文件信息转换为文件对象显示(只读模式)
selectedFiles = data.files.map(f => ({
name: f.file_name,
size: f.file_size,
isExisting: true
}));
renderFileList();
}
// 更新页面标题
document.title = `编辑数据集 - ${data.name || ''} - 远光软件微调平台`;
} catch (error) {
showMessage('错误', '加载数据集信息失败: ' + error.message, 'error');
}
}
// 初始化单选框选中样式 // 初始化单选框选中样式
function initRadioStyles() { function initRadioStyles() {
document.querySelectorAll('.radio-custom').forEach(radio => { document.querySelectorAll('.radio-custom').forEach(radio => {
@@ -436,50 +567,194 @@
} }
// 处理文件选择 // 处理文件选择
function handleFiles(files) { async function handleFiles(files) {
const validExtensions = ['.jsonl', '.xls', '.xlsx']; const validExtensions = ['.jsonl', '.json'];
const maxFileSize = 200 * 1024 * 1024; // 200MB const maxFileSize = 200 * 1024 * 1024; // 200MB
const maxFiles = 10; const maxFiles = 10;
files.forEach(file => { // 将FileList转换为数组
const fileArray = Array.from(files);
for (const file of fileArray) {
// 检查文件扩展名 // 检查文件扩展名
const ext = '.' + file.name.split('.').pop().toLowerCase(); const ext = '.' + file.name.split('.').pop().toLowerCase();
if (!validExtensions.includes(ext)) { if (!validExtensions.includes(ext)) {
alert(`文件 "${file.name}" 扩展名不支持,请上传 jsonl、xls 或 xlsx 格式的文件`); showMessage('提示', `文件 "${file.name}" 扩展名不支持,请上传 jsonl 或 json 格式的文件`, 'warning');
return; continue;
} }
// 检查文件大小 // 检查文件大小
if (file.size > maxFileSize) { if (file.size > maxFileSize) {
alert(`文件 "${file.name}" 大小超过200MB限制`); showMessage('提示', `文件 "${file.name}" 大小超过200MB限制`, 'warning');
return; continue;
} }
// 检查是否已存在相同文件 // 检查是否已存在相同文件
const exists = selectedFiles.some(f => f.name === file.name && f.size === file.size); const exists = selectedFiles.some(f => f.name === file.name);
if (exists) { if (exists) {
return; showMessage('提示', `文件 "${file.name}" 已存在`, 'warning');
continue;
} }
// 检查文件数量 // 检查文件数量
if (selectedFiles.length >= maxFiles) { if (selectedFiles.length >= maxFiles) {
alert('最多只能上传10个文件'); showMessage('提示', '最多只能上传10个文件', 'warning');
continue;
}
// 解析文件并统计记录数
try {
const fileInfo = await parseFileAndCount(file);
selectedFiles.push(fileInfo);
} catch (error) {
showMessage('错误', `解析文件 "${file.name}" 失败: ${error.message}`, 'error');
}
}
updateDataStats();
renderFileList();
}
// 解析文件并统计记录数
async function parseFileAndCount(file) {
return new Promise((resolve, reject) => {
const ext = file.name.split('.').pop().toLowerCase();
const reader = new FileReader();
if (ext === 'jsonl') {
// JSONL格式每行一个JSON对象
reader.onload = (e) => {
try {
const content = e.target.result;
// 限制读取内容用于预览100KB
const previewContent = content.substring(0, 102400);
const lines = content.trim().split('\n').filter(line => line.trim());
let recordCount = 0;
let isAlpacaFormat = true;
for (const line of lines) {
try {
const obj = JSON.parse(line);
recordCount++;
// 验证 Alpaca 格式:必须有 instruction 字段
if (!obj.instruction) {
isAlpacaFormat = false;
}
} catch (parseError) {
// 解析失败,跳过该行
}
}
resolve({
name: file.name,
size: file.size,
recordCount: recordCount,
isAlpacaFormat: isAlpacaFormat && recordCount > 0,
isExisting: false,
previewContent: previewContent,
file: file // 保存原始File对象
});
} catch (error) {
reject(new Error('文件格式错误'));
}
};
reader.readAsText(file);
} else if (ext === 'json') {
// JSON格式数组
reader.onload = (e) => {
try {
const content = e.target.result;
// 限制读取内容用于预览100KB
const previewContent = content.substring(0, 102400);
const data = JSON.parse(content);
let recordCount = 0;
let isAlpacaFormat = true;
// 支持数组格式
const items = Array.isArray(data) ? data : [data];
for (const item of items) {
recordCount++;
// 验证 Alpaca 格式:必须有 instruction 字段
if (!item.instruction) {
isAlpacaFormat = false;
}
}
resolve({
name: file.name,
size: file.size,
recordCount: recordCount,
isAlpacaFormat: isAlpacaFormat && recordCount > 0,
isExisting: false,
previewContent: previewContent,
file: file // 保存原始File对象
});
} catch (error) {
reject(new Error('JSON格式解析失败'));
}
};
reader.readAsText(file);
} else {
resolve({
name: file.name,
size: file.size,
recordCount: 0,
isAlpacaFormat: false,
isExisting: false,
previewContent: ''
});
}
});
}
// 更新数据统计
function updateDataStats() {
const dataStats = document.getElementById('dataStats');
const formatCheck = document.getElementById('formatCheck');
const formatError = document.getElementById('formatError');
if (selectedFiles.length === 0) {
dataStats.classList.add('hidden');
return; return;
} }
selectedFiles.push(file); dataStats.classList.remove('hidden');
});
// 检查所有文件格式
const hasAlpacaFiles = selectedFiles.some(f => f.isAlpacaFormat);
const hasNonAlpacaFiles = selectedFiles.some(f => !f.isAlpacaFormat && f.recordCount > 0);
if (hasNonAlpacaFiles) {
formatCheck.classList.add('hidden');
formatError.classList.remove('hidden');
} else if (hasAlpacaFiles) {
formatCheck.classList.remove('hidden');
formatError.classList.add('hidden');
} else {
formatCheck.classList.add('hidden');
formatError.classList.add('hidden');
}
}
// 清空所有文件
function clearAllFiles() {
selectedFiles = [];
updateDataStats();
renderFileList(); renderFileList();
} }
// 渲染文件列表 // 渲染文件列表
function renderFileList() { function renderFileList() {
const fileListEl = document.getElementById('fileList'); const fileListEl = document.getElementById('fileList');
const fileCountEl = document.getElementById('fileCount'); const uploadArea = document.getElementById('upload-area');
// 更新文件计数 // 如果有文件,隐藏上传区域
fileCountEl.textContent = selectedFiles.length; if (selectedFiles.length > 0) {
uploadArea.classList.add('hidden');
} else {
uploadArea.classList.remove('hidden');
}
if (selectedFiles.length === 0) { if (selectedFiles.length === 0) {
fileListEl.innerHTML = ''; fileListEl.innerHTML = '';
@@ -489,17 +764,27 @@
fileListEl.innerHTML = selectedFiles.map((file, index) => { fileListEl.innerHTML = selectedFiles.map((file, index) => {
const size = formatFileSize(file.size); const size = formatFileSize(file.size);
const icon = getFileIcon(file.name); const icon = getFileIcon(file.name);
const isExisting = file.isExisting;
const recordCount = file.recordCount || 0;
return ` return `
<div class="flex items-center justify-between bg-gray-50 px-4 py-2 rounded-lg border border-gray-200"> <div class="flex items-center justify-between bg-gray-50 px-4 py-2 rounded-lg border ${isExisting ? 'border-blue-200 bg-blue-50/50' : 'border-gray-200'}">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<i class="fa ${icon} text-primary text-lg"></i> <i class="fa ${icon} ${isExisting ? 'text-blue-500' : 'text-primary'} text-lg"></i>
<span class="text-sm text-gray-700 truncate max-w-md" title="${file.name}">${file.name}</span> <span class="text-sm ${isExisting ? 'text-blue-700' : 'text-gray-700'} truncate max-w-[150px]" title="${file.name}">${file.name}</span>
<span class="text-xs text-gray-400">${size}</span> <span class="text-xs ${isExisting ? 'text-blue-400' : 'text-gray-400'}">${size}</span>
${recordCount > 0 ? `<span class="text-xs px-2 py-0.5 bg-green-100 text-green-600 rounded">${recordCount} 条</span>` : ''}
${isExisting ? '<span class="text-xs px-2 py-0.5 bg-blue-100 text-blue-600 rounded">已上传</span>' : ''}
</div> </div>
<button type="button" onclick="removeFile(${index})" class="text-gray-400 hover:text-red-500 transition-colors" title="删除文件"> <div class="flex items-center space-x-2">
${!isExisting || file.previewContent ? `<button type="button" onclick="previewFile(${index})" class="${isExisting ? 'text-blue-400 hover:text-blue-600' : 'text-gray-400 hover:text-primary'} transition-colors" title="预览">
<i class="fa fa-eye text-lg"></i>
</button>` : ''}
<button type="button" onclick="removeFile(${index})" class="${isExisting ? 'text-blue-400 hover:text-blue-600' : 'text-gray-400 hover:text-red-500'} transition-colors" title="${isExisting ? '移除文件' : '删除文件'}">
<i class="fa fa-trash-o text-lg"></i> <i class="fa fa-trash-o text-lg"></i>
</button> </button>
</div> </div>
</div>
`; `;
}).join(''); }).join('');
} }
@@ -511,6 +796,63 @@
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
} }
// 预览文件内容
function previewFile(index) {
const file = selectedFiles[index];
if (!file) return;
if (file.isExisting) {
// 从服务器获取文件内容
const modal = document.getElementById('previewModal');
const modalTitle = document.getElementById('previewModalTitle');
const modalContent = document.getElementById('previewModalContent');
modalTitle.textContent = file.name;
modalContent.innerHTML = '<div class="flex justify-center items-center h-32"><span class="text-gray-500">加载中...</span></div>';
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
fetch(`/api/dataset-manage/preview/${file.id}`)
.then(res => res.json())
.then(data => {
if (data.code === 0) {
modalContent.innerHTML = `<pre class="text-xs text-gray-600 whitespace-pre-wrap break-all">${escapeHtml(data.data.content || '')}</pre>`;
} else {
modalContent.innerHTML = `<div class="text-red-500">${escapeHtml(data.message || '读取文件失败')}</div>`;
}
})
.catch(err => {
modalContent.innerHTML = `<div class="text-red-500">读取文件失败: ${escapeHtml(err.message)}</div>`;
});
return;
}
// 显示预览弹窗
const modal = document.getElementById('previewModal');
const modalTitle = document.getElementById('previewModalTitle');
const modalContent = document.getElementById('previewModalContent');
modalTitle.textContent = file.name;
modalContent.innerHTML = `<pre class="text-xs text-gray-600 whitespace-pre-wrap break-all">${escapeHtml(file.previewContent || '')}</pre>`;
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
// 关闭预览弹窗
function closePreviewModal() {
const modal = document.getElementById('previewModal');
modal.classList.add('hidden');
document.body.style.overflow = '';
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 获取文件图标 // 获取文件图标
function getFileIcon(filename) { function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase(); const ext = filename.split('.').pop().toLowerCase();
@@ -525,6 +867,7 @@
// 删除文件 // 删除文件
function removeFile(index) { function removeFile(index) {
selectedFiles.splice(index, 1); selectedFiles.splice(index, 1);
updateDataStats();
renderFileList(); renderFileList();
} }
@@ -552,12 +895,12 @@
const secretKey = document.querySelector('input[name="minio_secret_key"]').value; const secretKey = document.querySelector('input[name="minio_secret_key"]').value;
if (!endpoint || !bucket || !accessKey || !secretKey) { if (!endpoint || !bucket || !accessKey || !secretKey) {
alert('请填写完整的MinIO配置信息'); showMessage('提示', '请填写完整的MinIO配置信息', 'warning');
return; return;
} }
// 模拟测试连接 // 模拟测试连接
alert('正在测试连接...\n\n连接成功MinIO服务可用。'); showMessage('成功', '正在测试连接...\n\n连接成功MinIO服务可用。', 'success');
} }
// 提交表单 // 提交表单
@@ -565,19 +908,35 @@
const form = document.getElementById('datasetForm'); const form = document.getElementById('datasetForm');
const formData = new FormData(form); const formData = new FormData(form);
const storageValue = formData.get('storage'); const storageValue = formData.get('storage');
const data = {
name: formData.get('name'),
dataset_type: formData.get('dataset_type'),
storage: storageValue
};
// 验证文件 // 验证名称
if (selectedFiles.length === 0) { if (!formData.get('name')) {
alert('请选择至少一个文件上传'); showMessage('提示', '请输入数据集名称', 'warning');
return; return;
} }
// 如果选择MinIO存储添加MinIO配置 // 如果选择MinIO存储验证配置
if (storageValue === 'minio') {
const endpoint = formData.get('minio_endpoint');
const bucket = formData.get('minio_bucket');
const accessKey = formData.get('minio_access_key');
const secretKey = formData.get('minio_secret_key');
if (!endpoint || !bucket || !accessKey || !secretKey) {
showMessage('提示', '请填写完整的MinIO配置信息', 'warning');
return;
}
}
try {
// 准备数据
const data = {
name: formData.get('name'),
type: formData.get('dataset_type'),
storage_type: storageValue,
description: formData.get('description') || ''
};
if (storageValue === 'minio') { if (storageValue === 'minio') {
data.minio_config = { data.minio_config = {
endpoint: formData.get('minio_endpoint'), endpoint: formData.get('minio_endpoint'),
@@ -586,44 +945,222 @@
secret_key: formData.get('minio_secret_key'), secret_key: formData.get('minio_secret_key'),
ssl: formData.get('minio_ssl') === 'on' ssl: formData.get('minio_ssl') === 'on'
}; };
}
// 验证MinIO配置 // 判断是创建还是更新
if (!data.minio_config.endpoint || !data.minio_config.bucket || !data.minio_config.access_key || !data.minio_config.secret_key) { if (editId) {
alert('请填写完整的MinIO配置信息'); // 编辑模式使用PUT更新
const updateResponse = await fetch(`${API_BASE}/dataset-manage/${editId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const updateResult = await updateResponse.json();
if (updateResult.code !== 0) {
showMessage('错误', updateResult.message || '更新数据集失败', 'error');
return;
}
// 上传新文件(如果有)
const newFiles = selectedFiles.filter(f => !f.isExisting);
if (newFiles.length > 0) {
const uploadFormData = new FormData();
for (const fileObj of newFiles) {
uploadFormData.append('files', fileObj.file);
}
const uploadResponse = await fetch(`${API_BASE}/dataset-manage/upload/${editId}`, {
method: 'POST',
body: uploadFormData
});
const uploadResult = await uploadResponse.json();
if (uploadResult.code !== 0) {
showMessage('错误', '数据集更新成功,但新文件上传失败: ' + uploadResult.message, 'error');
setTimeout(() => {
window.location.href = 'main.html?page=dataset-manage';
}, 1500);
return; return;
} }
} }
if (!data.name) { showMessage('成功', '更新成功!', 'success', () => {
alert('请输入数据集名称'); window.location.href = 'main.html?page=dataset-manage';
});
} else {
// 创建模式
// 验证文件
if (selectedFiles.length === 0) {
showMessage('提示', '请选择至少一个文件上传', 'warning');
return; return;
} }
// 添加文件信息到请求数据 // 验证记录数
data.files = selectedFiles.map(file => ({ const totalRecords = selectedFiles.reduce((sum, f) => sum + (f.recordCount || 0), 0);
name: file.name, if (totalRecords === 0) {
size: file.size, showMessage('提示', '无法解析文件中的数据记录请确保文件格式正确JSON/JSONL格式需要包含 instruction 字段)', 'warning');
type: file.type return;
})); }
// 计算总文件大小
const totalSize = selectedFiles.reduce((sum, f) => sum + (f.size || 0), 0);
// 添加记录数和大小到数据中
data.count = totalRecords;
data.size = formatFileSize(totalSize);
try {
const response = await fetch(`${API_BASE}/dataset-manage`, { const response = await fetch(`${API_BASE}/dataset-manage`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
const result = await response.json(); const result = await response.json();
if (result.code === 0) {
alert('创建成功!'); if (result.code !== 0) {
showMessage('错误', result.message || '创建数据集失败', 'error');
return;
}
const datasetId = result.id;
// 上传文件
if (selectedFiles.length > 0) {
const uploadFormData = new FormData();
for (const fileObj of selectedFiles) {
uploadFormData.append('files', fileObj.file);
}
const uploadResponse = await fetch(`${API_BASE}/dataset-manage/upload/${datasetId}`, {
method: 'POST',
body: uploadFormData
});
const uploadResult = await uploadResponse.json();
if (uploadResult.code !== 0) {
showMessage('错误', '数据集创建成功,但文件上传失败: ' + uploadResult.message, 'error');
setTimeout(() => {
window.location.href = 'main.html?page=dataset-manage'; window.location.href = 'main.html?page=dataset-manage';
} else { }, 1500);
alert(result.message || '创建失败'); return;
} }
// 更新数据集的count字段数据记录数
const newTotalRecords = selectedFiles.reduce((sum, f) => sum + (f.recordCount || 0), 0);
await fetch(`${API_BASE}/dataset-manage/${datasetId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ count: newTotalRecords })
});
}
showMessage('成功', '创建成功!', 'success', () => {
window.location.href = 'main.html?page=dataset-manage';
});
}
} catch (error) { } catch (error) {
alert('创建失败: ' + error.message); showMessage('错误', editId ? '更新失败: ' + error.message : '创建失败: ' + error.message, 'error');
} }
} }
</script>
<!-- 预览弹窗 -->
<div id="previewModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="if(event.target === this) closePreviewModal();">
<div class="bg-white rounded-xl shadow-xl max-w-4xl w-full mx-4 overflow-hidden transform transition-all max-h-[80vh] flex flex-col">
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
<h3 id="previewModalTitle" class="text-lg font-medium text-gray-800 truncate"></h3>
<button onclick="closePreviewModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<div id="previewModalContent" class="flex-1 overflow-auto p-6 bg-gray-50">
</div>
<div class="px-6 py-3 border-t border-gray-100 bg-gray-50 flex justify-between items-center">
<span class="text-xs text-gray-400">仅显示前 100KB 内容</span>
<button onclick="closePreviewModal()" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">关闭</button>
</div>
</div>
</div>
<!-- 自定义消息弹窗 -->
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="if(event.target === this) closeModal();">
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden transform transition-all">
<div class="flex flex-col items-center justify-center min-h-[140px] py-4">
<div id="modalIcon"></div>
<h3 id="modalTitle" class="text-lg font-medium text-gray-800 mb-2"></h3>
<p id="modalMessage" class="text-gray-600 text-sm"></p>
</div>
<div id="modalBtnGroup" class="hidden px-6 pb-6 flex flex-col space-y-2 mx-4">
<button id="modalConfirmBtn" class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors">确定</button>
<button id="modalCancelBtn" class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">取消</button>
</div>
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
<button id="modalConfirmBtn2" class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors max-w-[160px]">确定</button>
</div>
</div>
</div>
<script>
// 显示消息弹窗
function showMessage(title, message, type = 'info', onConfirm) {
const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
const modalConfirmBtn2 = document.getElementById('modalConfirmBtn2');
const modalBtnGroup = document.getElementById('modalBtnGroup');
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
modalTitle.textContent = title;
modalTitle.className = 'text-lg font-medium text-gray-800 mb-2';
// 根据类型设置图标
if (type === 'success') {
modalIcon.innerHTML = '<div class="w-10 h-10 mx-auto mb-3 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-lg text-green-600"></i></div>';
} else if (type === 'error') {
modalIcon.innerHTML = '<div class="w-10 h-10 mx-auto mb-3 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-lg text-red-600"></i></div>';
} else if (type === 'warning') {
modalIcon.innerHTML = '<div class="w-10 h-10 mx-auto mb-3 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-lg text-yellow-600"></i></div>';
} else {
modalIcon.innerHTML = '<div class="w-10 h-10 mx-auto mb-3 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-lg text-blue-600"></i></div>';
}
modalMessage.innerHTML = message;
// 单按钮模式
modalBtnGroup.classList.add('hidden');
modalSingleBtnGroup.classList.remove('hidden');
const confirmBtn = modalConfirmBtn2;
confirmBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors max-w-[160px]';
if (type === 'success') {
confirmBtn.classList.add('bg-primary');
} else if (type === 'error') {
confirmBtn.className = 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-500/90 transition-colors max-w-[160px]';
} else if (type === 'warning') {
confirmBtn.className = 'px-6 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-500/90 transition-colors max-w-[160px]';
} else {
confirmBtn.classList.add('bg-primary');
}
confirmBtn.onclick = () => {
closeModal();
if (onConfirm) onConfirm();
};
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
// 关闭弹窗
function closeModal() {
const modal = document.getElementById('customModal');
modal.classList.add('hidden');
document.body.style.overflow = '';
}
// 返回上一页 // 返回上一页
function goBack() { function goBack() {
window.location.href = backUrl; window.location.href = backUrl;

View File

@@ -0,0 +1,341 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据管理-文件预览 - 远光软件微调平台</title>
<script src="../lib/tailwindcss/tailwind.js"></script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<style>
.sidebar-section-title {
padding: 0.5rem 1rem;
font-size: 0.75rem;
color: rgba(191, 203, 217, 0.7);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.nav-link:hover {
background-color: rgba(0, 21, 41, 0.2);
}
.sidebar-item-active {
background-color: rgba(24, 144, 255, 0.1);
color: #1890ff;
border-left: 4px solid #1890ff;
}
.text-primary { color: #1890ff; }
.bg-primary { background-color: #1890ff; }
.text-danger { color: #f5222d; }
</style>
</head>
<body class="antialiased bg-gray-50 flex h-screen overflow-hidden">
<!-- 侧边导航 -->
<aside class="w-64 text-[#bfcbd9] flex-shrink-0 hidden md:block flex flex-col h-full" style="background-color: #001529;">
<!-- 平台LOGO区域 -->
<div class="p-4 border-b border-[#001529]/30 flex items-center">
<img src="../assets/logo/logo.png" alt="Logo" class="w-6 h-6 object-contain mr-2">
<span class="text-white font-medium">远光软件微调平台</span>
</div>
<!-- 导航主区域 -->
<nav class="flex-1 overflow-y-auto py-2">
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<a href="#" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-cogs w-5 text-center"></i>
<span class="ml-2">模型调优</span>
</a>
<a href="#" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database w-5 text-center"></i>
<span class="ml-2">我的模型</span>
</a>
<a href="#" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-line-chart w-5 text-center"></i>
<span class="ml-2">模型评测</span>
</a>
<a href="#" data-page="model-deploy" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-server w-5 text-center"></i>
<span class="ml-2">模型对比</span>
</a>
<!-- 第二分区:资源管理 -->
<div class="sidebar-section-title mt-6">资源管理</div>
<a href="#" data-page="model-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-cube w-5 text-center"></i>
<span class="ml-2">模型管理</span>
</a>
<a href="#" data-page="dataset-manage" class="nav-link sidebar-item-active flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-file-text w-5 text-center"></i>
<span class="ml-2">数据集管理</span>
</a>
<a href="#" data-page="data-generate" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database w-5 text-center"></i>
<span class="ml-2">其他工具</span>
</a>
<!-- 第三分区:系统设置 -->
<div class="sidebar-section-title mt-6">系统设置</div>
<a href="#" data-page="config" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-bar-chart w-5 text-center"></i>
<span class="ml-2">平台性能</span>
</a>
</nav>
<!-- 底部信息区域 -->
<div class="p-4 border-t border-[#001529]/30 text-xs mt-auto">
<div class="mb-2 text-[#bfcbd9]/80">默认业务空间</div>
<div class="flex items-center justify-between">
<span class="text-[#bfcbd9]">版本 v1.0.0</span>
<i class="fa fa-question-circle-o text-[#bfcbd9]/70"></i>
</div>
</div>
</aside>
<!-- 主内容区 -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">
<div class="flex items-center justify-between px-6 h-14">
<div class="flex items-center space-x-6">
<a href="#" onclick="goBack()" class="text-gray-500 hover:text-gray-700 flex items-center px-3 py-1.5 rounded hover:bg-gray-100 transition-colors">
<i class="fa fa-arrow-left"></i>
<span class="ml-1">返回</span>
</a>
</div>
<div class="flex items-center space-x-4">
<div class="relative group">
<img src="https://picsum.photos/id/1005/32/32" class="w-8 h-8 rounded-full cursor-pointer" alt="用户头像">
<div class="absolute right-0 top-full mt-1 bg-white rounded shadow-lg py-1 hidden group-hover:block border border-gray-100 min-w-[140px] z-50">
<a href="login.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap">
<i class="fa fa-sign-out mr-1"></i>退出登录
</a>
</div>
</div>
</div>
</div>
</header>
<!-- 内容区域 - 文件预览 -->
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
<div id="page-content">
<!-- 文件预览卡片 -->
<div class="bg-white rounded-lg shadow-sm">
<!-- 面包屑导航 -->
<div class="px-4 py-3 border-b border-gray-100">
<div class="flex items-center text-sm">
<span class="text-primary cursor-pointer hover:underline" onclick="goBack()">数据集管理</span>
<span class="mx-2 text-gray-300">/</span>
<span class="text-gray-800 font-medium">文件预览</span>
</div>
</div>
<!-- 头部 -->
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<div class="flex items-center">
<h2 class="text-lg font-medium">文件预览</h2>
<span id="fileNameDisplay" class="ml-3 text-sm text-gray-500"></span>
</div>
<div class="flex items-center space-x-3">
<button onclick="downloadFile()" class="px-3 py-1.5 bg-primary text-white rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-download mr-1"></i>导出
</button>
<button onclick="deleteDataset()" class="px-3 py-1.5 border border-red-200 text-red-600 rounded text-sm hover:bg-red-50 transition-colors">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<!-- 状态栏 -->
<div id="fileStatus" class="px-4 py-3 bg-gray-50 border-b border-gray-100 flex items-center gap-6 text-sm text-gray-600 flex-wrap">
<!-- 动态生成 -->
</div>
<!-- 左侧文件列表 + 右侧预览区域 -->
<div class="flex" style="min-height: 500px;">
<!-- 右侧预览区域 -->
<div class="flex-1 flex flex-col">
<div class="p-3 border-b border-gray-100 bg-gray-50/30">
<h3 class="text-sm font-medium text-gray-700">预览内容</h3>
</div>
<div class="flex-1 p-4 overflow-auto">
<pre id="codePreview" class="text-sm text-gray-700 whitespace-pre-wrap font-mono bg-gray-50 rounded p-4" style="min-height: 400px;">加载中...</pre>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
// 动态获取 API 基础地址
const getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:8080/api`;
};
const API_BASE = getApiBase();
// 获取URL参数
const urlParams = new URLSearchParams(window.location.search);
const datasetId = urlParams.get('id');
const fileId = urlParams.get('fileId');
let currentDataset = null;
let selectedFileId = null;
// 返回上一页面
function goBack() {
window.location.href = 'main.html?page=dataset-manage';
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 绑定导航点击事件
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const page = this.dataset.page;
window.location.href = `main.html?page=${page}`;
});
});
// 加载数据集信息
loadDataset();
});
// 加载数据集信息
async function loadDataset() {
if (!datasetId) {
document.getElementById('codePreview').textContent = '缺少数据集ID参数';
return;
}
try {
const response = await fetch(`${API_BASE}/dataset-manage/${datasetId}`);
const result = await response.json();
if (result.code !== 0) {
document.getElementById('codePreview').textContent = '获取数据集信息失败: ' + (result.message || '未知错误');
return;
}
currentDataset = result.data;
updateUI();
} catch (error) {
document.getElementById('codePreview').textContent = '加载失败: ' + error.message;
}
}
// 更新UI
function updateUI() {
const dataset = currentDataset;
// 更新状态栏
const statusTag = dataset.storage_type === 'local'
? '<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-700">本地存储</span>'
: '<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-700">云存储</span>';
const count = dataset.count || 0;
const type = dataset.type || 'JSON/JSONL';
const createTime = dataset.create_time
? new Date(dataset.create_time).toLocaleString('zh-CN')
: '-';
document.getElementById('fileStatus').innerHTML = `
<span class="flex items-center">${statusTag}</span>
<span class="flex items-center">数据量:<span class="text-gray-800 ml-1">${count} 条</span></span>
<span class="flex items-center">数据类型:<span class="text-gray-800 ml-1">${type}</span></span>
<span class="flex items-center">创建时间:<span class="text-gray-800 ml-1">${createTime}</span></span>
`;
// 如果有文件ID选择该文件否则选择第一个文件
const targetFileId = fileId || (dataset.files && dataset.files.length > 0 ? dataset.files[0].id : null);
if (targetFileId) {
selectFile(targetFileId);
}
}
// 选择文件
async function selectFile(id) {
selectedFileId = id;
const previewEl = document.getElementById('codePreview');
previewEl.textContent = '加载中...';
try {
const response = await fetch(`${API_BASE}/dataset-manage/preview/${id}`);
const result = await response.json();
if (result.code !== 0) {
previewEl.textContent = '获取文件预览失败: ' + (result.message || '未知错误');
return;
}
const fileData = result.data;
document.getElementById('fileNameDisplay').textContent = fileData.file_name || '';
// 处理内容限制显示前100条记录
let content = fileData.content || '文件内容为空';
if (content && content.trim()) {
try {
// 尝试解析JSONL格式每行一个JSON对象
const lines = content.split('\n').filter(line => line.trim());
if (lines.length > 100) {
// 限制显示前100条
const limitedLines = lines.slice(0, 100);
content = limitedLines.join('\n') + '\n\n... 共 ' + lines.length + ' 条记录,已显示前 100 条 ...';
}
} catch (e) {
// 如果解析失败,直接显示原内容
}
}
previewEl.textContent = content;
} catch (error) {
previewEl.textContent = '加载失败: ' + error.message;
}
}
// 下载文件
function downloadFile() {
if (!selectedFileId) {
alert('请先选择一个文件');
return;
}
const protocol = window.location.protocol;
const hostname = window.location.hostname;
window.open(`${protocol}//${hostname}:8080/api/dataset-manage/download/${datasetId}/${selectedFileId}`, '_blank');
}
// 删除数据集
function deleteDataset() {
if (!datasetId) {
alert('缺少数据集ID');
return;
}
if (!confirm('确定要删除这个数据集吗?此操作不可恢复。')) {
return;
}
fetch(`${API_BASE}/dataset-manage/${datasetId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
if (result.code === 0) {
alert('删除成功');
window.location.href = 'main.html?page=dataset-manage';
} else {
alert('删除失败: ' + (result.message || '未知错误'));
}
})
.catch(error => {
alert('删除失败: ' + error.message);
});
}
</script>
</body>
</html>

View File

@@ -749,19 +749,19 @@
}; };
if (!data.name) { if (!data.name) {
alert('请输入任务名称'); showMessage('提示', '请输入任务名称', 'warning');
return; return;
} }
if (!data.base_model) { if (!data.base_model) {
alert('请选择基础模型'); showMessage('提示', '请选择基础模型', 'warning');
return; return;
} }
if (!data.train_dataset_id) { if (!data.train_dataset_id) {
alert('请选择训练集'); showMessage('提示', '请选择训练集', 'warning');
return; return;
} }
if (validSplit === 'custom' && !data.valid_dataset_id) { if (validSplit === 'custom' && !data.valid_dataset_id) {
alert('请选择验证集'); showMessage('提示', '请选择验证集', 'warning');
return; return;
} }
@@ -773,15 +773,88 @@
}); });
const result = await response.json(); const result = await response.json();
if (result.code === 0) { if (result.code === 0) {
alert('创建成功!'); showMessage('成功', '创建成功!', 'success', () => {
window.location.href = 'main.html'; window.location.href = 'main.html';
});
} else { } else {
alert(result.message || '创建失败'); showMessage('错误', result.message || '创建失败', 'error');
} }
} catch (error) { } catch (error) {
alert('创建失败: ' + error.message); showMessage('错误', '创建失败: ' + error.message, 'error');
} }
} }
</script> </script>
<!-- 自定义消息弹窗 -->
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="if(event.target === this) closeModal();">
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden transform transition-all">
<div class="flex flex-col items-center justify-center min-h-[160px] py-6">
<div id="modalIcon"></div>
<h3 id="modalTitle" class="text-base font-medium text-gray-800 mb-2"></h3>
<p id="modalMessage" class="text-gray-600 text-sm"></p>
</div>
<div id="modalBtnGroup" class="hidden px-6 pb-6 flex flex-col space-y-2 mx-4">
<button id="modalConfirmBtn" class="px-4 py-2 w-full text-white rounded transition-colors text-sm">确定</button>
<button id="modalCancelBtn" class="px-4 py-2 w-full border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors text-sm">取消</button>
</div>
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
<button id="modalConfirmBtn2" class="px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]">确定</button>
</div>
</div>
</div>
<script>
// 显示消息弹窗
function showMessage(title, message, type = 'info', onConfirm) {
const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
const modalConfirmBtn2 = document.getElementById('modalConfirmBtn2');
const modalBtnGroup = document.getElementById('modalBtnGroup');
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
modalTitle.textContent = title;
modalTitle.className = 'text-lg font-medium text-gray-800 mb-2';
if (type === 'success') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-xl text-green-600"></i></div>';
} else if (type === 'error') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-xl text-red-600"></i></div>';
} else if (type === 'warning') {
modalIcon.innerHTML = '<div 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>';
} else {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-xl text-blue-600"></i></div>';
}
modalMessage.innerHTML = message;
modalBtnGroup.classList.add('hidden');
modalSingleBtnGroup.classList.remove('hidden');
const confirmBtn = modalConfirmBtn2;
confirmBtn.className = 'px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]';
if (type === 'success') {
confirmBtn.classList.add('bg-primary');
} else if (type === 'error') {
confirmBtn.classList.add('bg-red-500');
} else if (type === 'warning') {
confirmBtn.classList.add('bg-yellow-500');
} else {
confirmBtn.classList.add('bg-primary');
}
confirmBtn.onclick = () => {
closeModal();
if (onConfirm) onConfirm();
};
modal.classList.remove('hidden');
}
function closeModal() {
const modal = document.getElementById('customModal');
modal.classList.add('hidden');
}
</script>
</body> </body>
</html> </html>

View File

@@ -310,9 +310,15 @@
columns: [ columns: [
{ title: '数据集名称', key: 'name' }, { title: '数据集名称', key: 'name' },
{ title: '类型', key: 'type' }, { title: '类型', key: 'type' },
{ title: '大小', key: 'size' }, { title: '存储位置', key: 'storage_type', render: (val) => {
{ title: '样本数', key: 'count' }, if (val === 'local') return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">本地存储</span>';
{ title: '描述', key: 'description' }, if (val === 'minio') return '<span class="px-2 py-1 rounded text-xs bg-orange-100 text-orange-700">MinIO</span>';
if (val === 'cloud') return '<span class="px-2 py-1 rounded text-xs bg-green-100 text-green-700">云存储</span>';
return '<span class="px-2 py-1 rounded text-xs bg-gray-100 text-gray-700">' + (val || '-') + '</span>';
}},
{ title: '大小', key: 'size', render: (val) => (val && val !== '0 B' && val !== '0') ? val : '-' },
{ title: '数据条数', key: 'count', render: (val) => val || 0 },
{ title: '描述', key: 'description', render: (val) => val || '-' },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' } { title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
], ],
actions: ['preview', 'download', 'delete'] actions: ['preview', 'download', 'delete']
@@ -484,6 +490,24 @@
}); });
} }
// 编辑数据集
function editItem(api, id) {
if (api === 'dataset-manage') {
// 跳转到数据集创建页面进行编辑
window.location.href = `dataset-create.html?id=${id}`;
} else {
showMessage('提示', '编辑功能开发中...', 'info');
}
}
// 下载数据集(打包下载)
function downloadDataset(datasetId) {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
const baseUrl = `${protocol}//${hostname}:8080`;
window.open(`${baseUrl}/api/dataset-manage/download/${datasetId}`, '_blank');
}
// 渲染表格页面 // 渲染表格页面
function renderTablePage(config, data) { function renderTablePage(config, data) {
const createButton = config.hasCreate ? ` const createButton = config.hasCreate ? `
@@ -521,12 +545,23 @@
`).join('')} `).join('')}
<td class="px-4 py-4 text-sm"> <td class="px-4 py-4 text-sm">
<div class="flex space-x-2"> <div class="flex space-x-2">
${config.actions.map(action => ` ${config.actions.map(action => {
<button onclick="${action === 'delete' ? `deleteItem('${config.api}', ${item.id})` : `showMessage('提示', '${actionLabels[action] || action}功能开发中...', 'info')`}" let onclick = '';
class="${action === 'delete' ? 'text-danger hover:text-danger/80' : 'text-primary hover:text-primary/80'}"> let btnClass = 'text-primary hover:text-primary/80';
${actionLabels[action] || action} if (action === 'delete') {
</button> onclick = `deleteItem('${config.api}', ${item.id})`;
`).join('')} btnClass = 'text-danger hover:text-danger/80';
} else if (action === 'edit') {
onclick = `editItem('${config.api}', ${item.id})`;
} else if (action === 'preview' && config.api === 'dataset-manage') {
onclick = `window.location.href = 'dataset-preview.html?id=${item.id}'`;
} else if (action === 'download' && config.api === 'dataset-manage') {
onclick = `downloadDataset('${item.id}')`;
} else {
onclick = `showMessage('提示', '${actionLabels[action] || action}功能开发中...', 'info')`;
}
return `<button onclick="${onclick}" class="${btnClass}">${actionLabels[action] || action}</button>`;
}).join('')}
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -435,11 +435,11 @@
}; };
if (!data.name) { if (!data.name) {
alert('请输入任务名称'); showMessage('提示', '请输入任务名称', 'warning');
return; return;
} }
if (!data.model_id) { if (!data.model_id) {
alert('请选择评测模型'); showMessage('提示', '请选择评测模型', 'warning');
return; return;
} }
@@ -451,15 +451,88 @@
}); });
const result = await response.json(); const result = await response.json();
if (result.code === 0) { if (result.code === 0) {
alert('创建成功!'); showMessage('成功', '创建成功!', 'success', () => {
window.location.href = 'main.html?page=model-eval'; window.location.href = 'main.html?page=model-eval';
});
} else { } else {
alert(result.message || '创建失败'); showMessage('错误', result.message || '创建失败', 'error');
} }
} catch (error) { } catch (error) {
alert('创建失败: ' + error.message); showMessage('错误', '创建失败: ' + error.message, 'error');
} }
} }
</script> </script>
<!-- 自定义消息弹窗 -->
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="if(event.target === this) closeModal();">
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden transform transition-all">
<div class="flex flex-col items-center justify-center min-h-[160px] py-6">
<div id="modalIcon"></div>
<h3 id="modalTitle" class="text-base font-medium text-gray-800 mb-2"></h3>
<p id="modalMessage" class="text-gray-600 text-sm"></p>
</div>
<div id="modalBtnGroup" class="hidden px-6 pb-6 flex flex-col space-y-2 mx-4">
<button id="modalConfirmBtn" class="px-4 py-2 w-full text-white rounded transition-colors text-sm">确定</button>
<button id="modalCancelBtn" class="px-4 py-2 w-full border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors text-sm">取消</button>
</div>
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
<button id="modalConfirmBtn2" class="px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]">确定</button>
</div>
</div>
</div>
<script>
// 显示消息弹窗
function showMessage(title, message, type = 'info', onConfirm) {
const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
const modalConfirmBtn2 = document.getElementById('modalConfirmBtn2');
const modalBtnGroup = document.getElementById('modalBtnGroup');
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
modalTitle.textContent = title;
modalTitle.className = 'text-lg font-medium text-gray-800 mb-2';
if (type === 'success') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-xl text-green-600"></i></div>';
} else if (type === 'error') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-xl text-red-600"></i></div>';
} else if (type === 'warning') {
modalIcon.innerHTML = '<div 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>';
} else {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-xl text-blue-600"></i></div>';
}
modalMessage.innerHTML = message;
modalBtnGroup.classList.add('hidden');
modalSingleBtnGroup.classList.remove('hidden');
const confirmBtn = modalConfirmBtn2;
confirmBtn.className = 'px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]';
if (type === 'success') {
confirmBtn.classList.add('bg-primary');
} else if (type === 'error') {
confirmBtn.classList.add('bg-red-500');
} else if (type === 'warning') {
confirmBtn.classList.add('bg-yellow-500');
} else {
confirmBtn.classList.add('bg-primary');
}
confirmBtn.onclick = () => {
closeModal();
if (onConfirm) onConfirm();
};
modal.classList.remove('hidden');
}
function closeModal() {
const modal = document.getElementById('customModal');
modal.classList.add('hidden');
}
</script>
</body> </body>
</html> </html>