Yolov/templates/model_upload.html

444 lines
17 KiB
HTML
Raw Normal View History

2025-12-12 16:04:22 +08:00
<!DOCTYPE html>
<html>
<head>
<title>模型文件上传</title>
<meta charset="UTF-8">
2025-12-16 10:08:12 +08:00
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
2025-12-12 16:04:22 +08:00
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.upload-section {
border: 2px dashed #ccc;
padding: 40px;
text-align: center;
margin-bottom: 20px;
border-radius: 8px;
}
.upload-section.dragover {
border-color: #007bff;
background-color: #f0f8ff;
}
.progress-container {
margin-top: 20px;
display: none;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #f5f5f5;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #007bff;
width: 0%;
transition: width 0.3s ease;
}
.status-info {
margin-top: 10px;
color: #666;
}
.upload-list {
margin-top: 30px;
}
.model-item {
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.btn:hover {
background-color: #0056b3;
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.error {
color: #dc3545;
margin-top: 5px;
}
.success {
color: #28a745;
margin-top: 5px;
}
</style>
</head>
<body>
<h1>加密模型文件上传</h1>
<div class="form-group">
<label for="encryptionKey">加密密钥(可选,建议使用)</label>
<input type="password" id="encryptionKey" placeholder="输入加密密钥(如果不提供将自动生成)">
<button onclick="generateKey()" class="btn">生成密钥</button>
<div id="keyInfo" class="success" style="display: none;"></div>
</div>
<div class="upload-section" id="dropArea">
<h3>拖放模型文件到此处,或点击选择文件</h3>
<p>支持 .pt, .pth, .onnx, .engine 格式,最大 1GB</p>
<input type="file" id="fileInput" style="display: none;">
<button onclick="document.getElementById('fileInput').click()" class="btn">选择文件</button>
<div id="fileInfo" style="margin-top: 10px;"></div>
</div>
<div class="progress-container" id="progressContainer">
<h3>上传进度</h3>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="status-info">
<span id="progressText">0%</span>
<span id="chunkInfo">(0/0 分片)</span>
</div>
<div id="uploadStatus" class="status-info"></div>
<button onclick="cancelUpload()" class="btn" id="cancelBtn" style="display: none; margin-top: 10px;">取消上传</button>
</div>
<div class="upload-list">
<h3>已上传的模型文件</h3>
<button onclick="loadModels()" class="btn">刷新列表</button>
<div id="modelList"></div>
</div>
<script>
let currentSessionId = null;
let currentFile = null;
let encryptionKey = '';
let uploadInProgress = false;
// 元素引用
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const progressContainer = document.getElementById('progressContainer');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const chunkInfo = document.getElementById('chunkInfo');
const uploadStatus = document.getElementById('uploadStatus');
const cancelBtn = document.getElementById('cancelBtn');
const fileInfo = document.getElementById('fileInfo');
const keyInfo = document.getElementById('keyInfo');
const modelList = document.getElementById('modelList');
const encryptionKeyInput = document.getElementById('encryptionKey');
// 生成加密密钥
async function generateKey() {
try {
const response = await axios.post('/api/models/generate_key');
if (response.data.status === 'success') {
const key = response.data.data.key;
encryptionKeyInput.value = key;
keyInfo.textContent = `已生成密钥: ${key.substring(0, 20)}...`;
keyInfo.style.display = 'block';
keyInfo.className = 'success';
}
} catch (error) {
keyInfo.textContent = '生成密钥失败: ' + (error.response?.data?.message || error.message);
keyInfo.style.display = 'block';
keyInfo.className = 'error';
}
}
// 拖放功能
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('dragover');
});
dropArea.addEventListener('dragleave', (e) => {
e.preventDefault();
dropArea.classList.remove('dragover');
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFileSelect(e.dataTransfer.files[0]);
}
});
// 文件选择
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelect(e.target.files[0]);
}
});
// 处理文件选择
function handleFileSelect(file) {
if (uploadInProgress) {
alert('请等待当前上传完成');
return;
}
// 检查文件扩展名
const allowedExtensions = ['.pt', '.pth', '.onnx', '.engine'];
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(fileExt)) {
alert('不支持的文件格式,请上传模型文件');
return;
}
// 检查文件大小
const maxSize = 1024 * 1024 * 1024; // 1GB
if (file.size > maxSize) {
alert('文件太大最大支持1GB');
return;
}
currentFile = file;
encryptionKey = encryptionKeyInput.value.trim();
fileInfo.innerHTML = `
<div class="success">
<strong>已选择文件:</strong> ${file.name}<br>
<strong>文件大小:</strong> ${formatFileSize(file.size)}<br>
<strong>加密密钥:</strong> ${encryptionKey ? '已提供' : '未提供(建议提供)'}
</div>
<button onclick="startUpload()" class="btn" style="margin-top: 10px;">开始上传</button>
`;
}
// 开始上传
async function startUpload() {
if (!currentFile) {
alert('请先选择文件');
return;
}
try {
uploadInProgress = true;
// 1. 创建上传会话
const startResponse = await axios.post('/api/models/upload/start', {
filename: currentFile.name,
total_size: currentFile.size,
encryption_key: encryptionKey || null
});
if (startResponse.data.status !== 'success') {
throw new Error(startResponse.data.message);
}
const { session_id, total_chunks, chunk_size } = startResponse.data.data;
currentSessionId = session_id;
// 显示进度条
progressContainer.style.display = 'block';
cancelBtn.style.display = 'block';
// 2. 分片上传
for (let chunkIndex = 0; chunkIndex < total_chunks; chunkIndex++) {
if (!uploadInProgress) {
break; // 用户取消了上传
}
const start = chunkIndex * chunk_size;
const end = Math.min(start + chunk_size, currentFile.size);
const chunk = currentFile.slice(start, end);
const formData = new FormData();
formData.append('session_id', session_id);
formData.append('chunk_index', chunkIndex);
formData.append('chunk', chunk);
try {
const chunkResponse = await axios.post('/api/models/upload/chunk', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
const chunkProgress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
updateProgress(chunkIndex + 1, total_chunks, chunkProgress);
}
});
if (chunkResponse.data.status !== 'success') {
throw new Error(chunkResponse.data.message);
}
// 更新整体进度
const progress = chunkResponse.data.data.progress;
updateProgress(chunkIndex + 1, total_chunks, progress);
} catch (chunkError) {
console.error('分片上传失败:', chunkError);
uploadStatus.innerHTML = `<div class="error">分片 ${chunkIndex} 上传失败: ${chunkError.message}</div>`;
break;
}
}
if (uploadInProgress) {
// 3. 轮询上传状态直到完成
await pollUploadStatus(session_id);
}
} catch (error) {
console.error('上传失败:', error);
uploadStatus.innerHTML = `<div class="error">上传失败: ${error.message}</div>`;
} finally {
if (!uploadInProgress) {
uploadStatus.innerHTML = `<div class="error">上传已取消</div>`;
}
loadModels(); // 刷新模型列表
}
}
// 更新进度显示
function updateProgress(currentChunk, totalChunks, progress) {
const overallProgress = Math.round(((currentChunk - 1) / totalChunks) * 100 + (progress / totalChunks));
progressFill.style.width = overallProgress + '%';
progressText.textContent = overallProgress + '%';
chunkInfo.textContent = `(${currentChunk}/${totalChunks} 分片)`;
}
// 轮询上传状态
async function pollUploadStatus(sessionId) {
let attempts = 0;
const maxAttempts = 60; // 最多等待5分钟 (60 * 5秒)
while (attempts < maxAttempts && uploadInProgress) {
try {
const statusResponse = await axios.get(`/api/models/upload/status/${sessionId}`);
if (statusResponse.data.status === 'success') {
const statusData = statusResponse.data.data;
if (statusData.status === 'completed') {
uploadStatus.innerHTML = `
<div class="success">
<strong>上传完成!</strong><br>
加密文件: ${statusData.relative_path}<br>
模型哈希: ${statusData.model_hash}<br>
密钥哈希: ${statusData.key_hash}
</div>
`;
currentSessionId = null;
uploadInProgress = false;
return;
} else if (statusData.status === 'failed') {
uploadStatus.innerHTML = `<div class="error">上传失败: ${statusData.error}</div>`;
currentSessionId = null;
uploadInProgress = false;
return;
} else if (statusData.status === 'merging' || statusData.status === 'encrypting') {
uploadStatus.innerHTML = `<div class="success">正在${statusData.status === 'merging' ? '合并' : '加密'}文件...</div>`;
}
}
attempts++;
await sleep(5000); // 每5秒轮询一次
} catch (error) {
console.error('获取状态失败:', error);
await sleep(5000);
}
}
if (attempts >= maxAttempts) {
uploadStatus.innerHTML = `<div class="error">上传超时</div>`;
}
}
// 取消上传
function cancelUpload() {
if (currentSessionId && uploadInProgress) {
if (confirm('确定要取消上传吗?')) {
uploadInProgress = false;
axios.post(`/api/models/upload/cancel/${currentSessionId}`);
uploadStatus.innerHTML = `<div class="error">上传已取消</div>`;
}
}
}
// 加载模型列表
async function loadModels() {
try {
const response = await axios.get('/api/models/list');
if (response.data.status === 'success') {
const models = response.data.data.models;
if (models.length === 0) {
modelList.innerHTML = '<p>暂无模型文件</p>';
return;
}
let html = '<table style="width:100%; border-collapse: collapse;">';
html += '<tr><th>文件名</th><th>大小</th><th>修改时间</th><th>操作</th></tr>';
models.forEach(model => {
const modifiedDate = new Date(model.modified * 1000).toLocaleString();
html += `
<tr class="model-item">
<td>${model.filename}</td>
<td>${formatFileSize(model.size)}</td>
<td>${modifiedDate}</td>
<td>
<button onclick="copyPath('${model.filename}')" class="btn" style="padding: 5px 10px; font-size: 12px;">复制路径</button>
</td>
</tr>
`;
});
html += '</table>';
modelList.innerHTML = html;
}
} catch (error) {
modelList.innerHTML = `<div class="error">加载模型列表失败: ${error.message}</div>`;
}
}
// 辅助函数
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function copyPath(filename) {
const path = `encrypted_models/${filename}`;
navigator.clipboard.writeText(path).then(() => {
alert('路径已复制到剪贴板: ' + path);
});
}
// 页面加载时加载模型列表
window.onload = loadModels;
</script>
</body>
</html>