Yolov/templates/model_upload.html

444 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html>
<head>
<title>模型文件上传</title>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<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>