Files
v-hono/public/index.html
T

1344 lines
52 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V-Hono - 轻量级 V 语言 Web 框架</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #fafafa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
color: black;
margin-bottom: 40px;
}
.header h1 {
font-size: 3em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2em;
opacity: 0.9;
}
.section {
background: rgba(255, 255, 255, 0.95);
margin: 20px 0;
padding: 30px;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.section h2 {
color: #2c5aa0;
margin-bottom: 20px;
font-size: 1.8em;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #667eea;
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-card h3 {
color: #2c5aa0;
margin-bottom: 10px;
}
.endpoint {
background: #e8f4fd;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
border-left: 4px solid #2196F3;
}
.endpoint strong {
color: #1976D2;
}
.endpoint a {
color: #2196F3;
text-decoration: none;
font-weight: bold;
}
.endpoint a:hover {
text-decoration: underline;
}
.test-area {
background: #f5f5f5;
padding: 20px;
border-radius: 10px;
margin: 15px 0;
}
.test-area h4 {
color: #2c5aa0;
margin-bottom: 10px;
}
.test-button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
transition: background 0.3s ease;
}
.test-button:hover {
background: #5a6fd8;
}
.test-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.result {
background: #e8f5e8;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #4CAF50;
white-space: pre-wrap;
font-family: monospace;
font-size: 0.9em;
max-height: 300px;
overflow-y: auto;
}
.error {
background: #ffebee;
border-left: 4px solid #f44336;
}
.loading {
background: #fff3e0;
border-left: 4px solid #ff9800;
}
.code-block {
background: #2d3748;
color: #e2e8f0;
padding: 20px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
overflow-x: auto;
margin: 15px 0;
}
.tab-container {
margin: 20px 0;
}
.tab-buttons {
display: flex;
border-bottom: 2px solid #ddd;
margin-bottom: 20px;
}
.tab-button {
background: none;
border: none;
padding: 10px 20px;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
}
.tab-button.active {
border-bottom-color: #667eea;
color: #667eea;
font-weight: bold;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
}
.stat-number {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.footer {
text-align: center;
color: white;
margin-top: 40px;
opacity: 0.8;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2em;
}
.feature-grid {
grid-template-columns: 1fr;
}
}
/* 分块上传相关样式 */
.upload-area {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 40px;
text-align: center;
margin: 20px 0;
transition: border-color 0.3s;
}
.upload-area:hover {
border-color: #007bff;
}
.upload-area.dragover {
border-color: #007bff;
background-color: #f8f9fa;
}
.file-info {
margin: 10px 0;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
display: none;
}
.progress-container {
margin: 20px 0;
display: none;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #28a745);
width: 0%;
transition: width 0.3s;
}
.chunk-status {
margin: 10px 0;
font-size: 14px;
color: #666;
}
.status.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 V-Hono</h1>
<p>轻量级 V 语言 Web 框架 - 功能完整展示</p>
</div>
<!-- 核心特性展示 -->
<div class="section">
<h2>✨ 核心特性</h2>
<div class="feature-grid">
<div class="feature-card">
<h3>🚀 高性能路由</h3>
<p>支持 Trie 路由树和 Hybrid 路由,支持静态路由、动态参数、通配符和正则路由</p>
</div>
<div class="feature-card">
<h3>📁 静态文件服务</h3>
<p>内置静态文件服务中间件,支持多目录、缓存控制、ETag、路径安全防护</p>
</div>
<div class="feature-card">
<h3>🔧 洋葱模型中间件</h3>
<p>支持洋葱模型中间件,提供日志、认证、CORS 等常用中间件</p>
</div>
<div class="feature-card">
<h3>🛡️ 安全防护</h3>
<p>内置路径遍历攻击防护、点文件访问控制、请求验证等安全特性</p>
</div>
<div class="feature-card">
<h3>📊 性能监控</h3>
<p>内置路由性能统计、缓存命中率监控、请求响应时间统计</p>
</div>
<div class="feature-card">
<h3>🔄 HTTP 方法支持</h3>
<p>完整支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等 HTTP 方法</p>
</div>
<div class="feature-card">
<h3>📤 分块上传</h3>
<p>支持大文件分片上传、断点续传、秒传功能,内置文件完整性验证</p>
</div>
</div>
</div>
<!-- 功能测试区域 -->
<div class="section">
<h2>🧪 功能测试</h2>
<!-- Token 输入区域 -->
<div class="test-area">
<h4>🔑 认证 Token</h4>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 15px;">
<input type="text" id="token-input" placeholder="请输入 token" value="abc123" style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<button class="test-button" onclick="setToken()">设置 Token</button>
<button class="test-button" onclick="clearToken()">清除 Token</button>
</div>
<div id="token-status" style="font-size: 0.9em; color: #666;">
当前 Token: <span id="current-token">abc123</span>
</div>
</div>
<div class="tab-container">
<div class="tab-buttons">
<button class="tab-button active" onclick="showTab('api')">API 测试</button>
<button class="tab-button" onclick="showTab('files')">文件服务</button>
<button class="tab-button" onclick="showTab('routes')">路由测试</button>
<button class="tab-button" onclick="showTab('static')">静态文件</button>
<button class="tab-button" onclick="showTab('upload')">分块上传</button>
</div>
<!-- API 测试 -->
<div id="api" class="tab-content active">
<div class="test-area">
<h4>健康检查</h4>
<button class="test-button" onclick="testAPI('/api/health')">测试 /api/health</button>
<button class="test-button" onclick="testAPI('/api/status')">测试 /api/status</button>
<button class="test-button" onclick="testAPI('/api/static-info')">测试 /api/static-info</button>
<div id="api-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 文件服务测试 -->
<div id="files" class="tab-content">
<div class="test-area">
<h4>文件服务测试</h4>
<button class="test-button" onclick="testFile('/file/test.txt')">基本文件服务</button>
<button class="test-button" onclick="testFile('/file-cached/test.txt')">缓存文件服务</button>
<button class="test-button" onclick="testFile('/download/downloadable.txt')">文件下载</button>
<button class="test-button" onclick="testFile('/image/img1.jpg')">图片文件服务</button>
<button class="test-button" onclick="testFile('/safe-file/test.txt')">安全文件服务</button>
<div id="file-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 路由测试 -->
<div id="routes" class="tab-content">
<div class="test-area">
<h4>动态路由测试</h4>
<button class="test-button" onclick="testRoute('/users/123')">单参数路由</button>
<button class="test-button" onclick="testRoute('/posts/456/comments/789')">多参数路由</button>
<button class="test-button" onclick="testRoute('/api/users/101/posts/202')">嵌套参数路由</button>
<div id="route-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 静态文件测试 -->
<div id="static" class="tab-content">
<div class="test-area">
<h4>静态文件访问</h4>
<button class="test-button" onclick="testStatic('/test.txt')">测试文本文件</button>
<button class="test-button" onclick="testStatic('/assets/style.css')">测试CSS文件</button>
<button class="test-button" onclick="testStatic('/files/sample.json')">测试JSON文件</button>
<div id="static-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 分块上传测试 -->
<div id="upload" class="tab-content">
<div class="test-area">
<h4>📤 分块上传功能</h4>
<p>支持大文件分片上传、断点续传、秒传功能</p>
<div class="upload-area" id="uploadArea">
<p>📁 拖拽文件到此处或点击选择文件</p>
<input type="file" id="fileInput" style="display: none;" multiple>
<button class="test-button" onclick="document.getElementById('fileInput').click()">选择文件</button>
</div>
<div class="file-info" id="fileInfo">
<h4>文件信息</h4>
<p><strong>文件名:</strong> <span id="fileName"></span></p>
<p><strong>文件大小:</strong> <span id="fileSize"></span></p>
<p><strong>分片数量:</strong> <span id="chunkCount"></span></p>
<p><strong>分片大小:</strong> <span id="chunkSize"></span></p>
</div>
<div class="progress-container" id="progressContainer">
<h4>上传进度</h4>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="chunk-status" id="chunkStatus"></div>
<div class="chunk-status" id="poolStatus" style="font-size: 12px; color: #888; margin-top: 5px;"></div>
</div>
<div class="status" id="uploadStatus"></div>
<button class="test-button" id="uploadBtn" onclick="startUpload()" style="display: none;">开始上传</button>
</div>
</div>
</div>
</div>
<!-- 可用端点 -->
<div class="section">
<h2>📋 可用端点</h2>
<h3>🔧 API 端点</h3>
<div class="endpoint">
<strong>健康检查:</strong> <a href="/api/health" target="_blank">/api/health</a>
</div>
<div class="endpoint">
<strong>静态文件状态:</strong> <a href="/api/status" target="_blank">/api/status</a>
</div>
<div class="endpoint">
<strong>静态文件信息:</strong> <a href="/api/static-info" target="_blank">/api/static-info</a>
</div>
<div class="endpoint">
<strong>基础路由:</strong> <a href="/hello" target="_blank">/hello</a>
</div>
<h3>📄 文件服务端点</h3>
<div class="endpoint">
<strong>基本文件服务:</strong> <a href="/file/test.txt" target="_blank">/file/test.txt</a>
</div>
<div class="endpoint">
<strong>缓存文件服务:</strong> <a href="/file-cached/test.txt" target="_blank">/file-cached/test.txt</a>
</div>
<div class="endpoint">
<strong>文件下载:</strong> <a href="/download/downloadable.txt" target="_blank">/download/downloadable.txt</a>
</div>
<div class="endpoint">
<strong>图片文件服务:</strong> <a href="/image/style.css" target="_blank">/image/style.css</a>
</div>
<div class="endpoint">
<strong>安全文件服务:</strong> <a href="/safe-file/test.txt" target="_blank">/safe-file/test.txt</a>
</div>
<h3>🎯 动态路由</h3>
<div class="endpoint">
<strong>单参数:</strong> <a href="/users/123" target="_blank">/users/123</a>
</div>
<div class="endpoint">
<strong>多参数:</strong> <a href="/posts/456/comments/789" target="_blank">/posts/456/comments/789</a>
</div>
<div class="endpoint">
<strong>嵌套参数:</strong> <a href="/api/users/101/posts/202" target="_blank">/api/users/101/posts/202</a>
</div>
<h3>📁 静态文件目录</h3>
<div class="endpoint">
<strong>默认目录:</strong> <a href="/test.txt" target="_blank">/test.txt</a> (来自 ./public)
</div>
<div class="endpoint">
<strong>资源目录:</strong> <a href="/assets/style.css" target="_blank">/assets/style.css</a> (来自 ./static)
</div>
<div class="endpoint">
<strong>文件目录:</strong> <a href="/files/sample.json" target="_blank">/files/sample.json</a> (来自 ./uploads)
</div>
<h3>📤 分块上传端点</h3>
<div class="endpoint">
<strong>分片上传:</strong> <a href="/upload/chunk" target="_blank">POST /upload/chunk</a>
</div>
<div class="endpoint">
<strong>上传状态:</strong> <a href="/upload/status" target="_blank">GET /upload/status</a>
</div>
<div class="endpoint">
<strong>分片查询:</strong> <a href="/upload/chunks" target="_blank">GET /upload/chunks</a>
</div>
<div class="endpoint">
<strong>分片检查:</strong> <a href="/upload/chunk_exists" target="_blank">GET /upload/chunk_exists</a>
</div>
<div class="endpoint">
<strong>文件管理:</strong> <a href="/api/files" target="_blank">GET /api/files</a>
</div>
</div>
<!-- 性能统计 -->
<div class="section">
<h2>📊 性能统计</h2>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="request-count">0</div>
<div>总请求数</div>
</div>
<div class="stat-card">
<div class="stat-number" id="success-rate">100%</div>
<div>成功率</div>
</div>
<div class="stat-card">
<div class="stat-number" id="avg-response">0ms</div>
<div>平均响应时间</div>
</div>
<div class="stat-card">
<div class="stat-number" id="cache-hit">0%</div>
<div>缓存命中率</div>
</div>
</div>
</div>
<!-- 代码示例 -->
<div class="section">
<h2>💻 代码示例</h2>
<div class="code-block">
// 基本路由设置
mut app := hono.Hono.new()
// 静态文件服务
app.use(hono.serve_static_default())
app.use(hono.serve_static_path('/assets', './static'))
// 基础路由
app.get('/hello', fn (mut c hono.Context) http.Response {
return c.text('Hello, World!')
})
// 动态路由
app.get('/users/:id', fn (mut c hono.Context) http.Response {
user_id := c.params['id']
return c.json('{"user_id": "${user_id}"}')
})
// 文件服务
app.get('/file/:filename', fn (mut c hono.Context) http.Response {
filename := c.params['filename']
return c.file('public/${filename}')
})
// 启动服务器
app.listen(':8080')
</div>
</div>
<div class="footer">
<p>🎉 V-Hono 静态文件服务正常运行中</p>
<p>这是来自 <code>./public</code> 目录的 <code>index.html</code> 文件</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
<script>
let requestCount = 0;
let successCount = 0;
let totalResponseTime = 0;
let currentToken = 'abc123'; // 默认 token
// Token 管理函数
function setToken() {
const tokenInput = document.getElementById('token-input');
const newToken = tokenInput.value.trim();
if (newToken) {
currentToken = newToken;
document.getElementById('current-token').textContent = currentToken;
showMessage('Token 已更新', 'success');
} else {
showMessage('请输入有效的 Token', 'error');
}
}
function clearToken() {
currentToken = '';
document.getElementById('token-input').value = '';
document.getElementById('current-token').textContent = '未设置';
showMessage('Token 已清除', 'warning');
}
function showMessage(message, type) {
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 5px;
color: white;
font-weight: bold;
z-index: 1000;
animation: slideIn 0.3s ease;
`;
switch(type) {
case 'success':
messageDiv.style.background = '#4CAF50';
break;
case 'error':
messageDiv.style.background = '#f44336';
break;
case 'warning':
messageDiv.style.background = '#ff9800';
break;
}
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease';
setTimeout(() => document.body.removeChild(messageDiv), 300);
}, 2000);
}
// 添加 CSS 动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
function showTab(tabName) {
// 隐藏所有标签内容
const tabContents = document.querySelectorAll('.tab-content');
tabContents.forEach(content => content.classList.remove('active'));
// 移除所有按钮的active类
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(button => button.classList.remove('active'));
// 显示选中的标签内容
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
}
async function testAPI(endpoint) {
const resultDiv = document.getElementById('api-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n响应数据:\n${data}`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testFile(endpoint) {
const resultDiv = document.getElementById('file-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const responseTime = Date.now() - startTime;
// 获取所有响应头信息用于调试
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
// 检查是否是图片文件 - 优先检查文件扩展名
const urlPath = url.pathname.toLowerCase();
const isImageByExtension = urlPath.endsWith('.jpg') || urlPath.endsWith('.jpeg') ||
urlPath.endsWith('.png') || urlPath.endsWith('.gif') ||
urlPath.endsWith('.webp') || urlPath.endsWith('.svg');
// 也检查 content-type
const contentType = response.headers.get('content-type') || '';
const isImageByType = contentType.startsWith('image/');
const isImage = isImageByExtension || isImageByType;
if (isImage) {
// 对于图片文件,显示图片预览
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
resultDiv.className = 'result';
resultDiv.innerHTML = `
✅ 成功 (${responseTime}ms)<br>
状态码: ${response.status}<br>
请求URL: ${url.toString()}<br>
文件类型: ${contentType}<br>
文件大小: ${(blob.size / 1024).toFixed(2)} KB<br>
检测方式: ${isImageByExtension ? '文件扩展名' : 'Content-Type'}<br>
<br>
<strong>响应头信息:</strong><br>
<pre style="font-size: 0.8em; background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">${JSON.stringify(headers, null, 2)}</pre>
<br>
<strong>图片预览:</strong><br>
<img src="${imageUrl}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 10px;" alt="图片预览">
`;
} else {
// 对于非图片文件,显示文本内容
const data = await response.text();
resultDiv.className = 'result';
resultDiv.innerHTML = `
✅ 成功 (${responseTime}ms)<br>
状态码: ${response.status}<br>
请求URL: ${url.toString()}<br>
文件类型: ${contentType}<br>
文件大小: ${data.length} 字符<br>
<br>
<strong>响应头信息:</strong><br>
<pre style="font-size: 0.8em; background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">${JSON.stringify(headers, null, 2)}</pre>
<br>
<strong>内容预览:</strong><br>
<pre style="font-size: 0.9em; background: #f9f9f9; padding: 10px; border-radius: 4px; overflow-x: auto;">${data.substring(0, 200)}${data.length > 200 ? '...' : ''}</pre>
`;
}
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testRoute(endpoint) {
const resultDiv = document.getElementById('route-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n路由参数解析:\n${data}`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testStatic(endpoint) {
const resultDiv = document.getElementById('static-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n文件类型: ${response.headers.get('content-type')}\n文件大小: ${data.length} 字符`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
function updateStats(success, responseTime) {
requestCount++;
if (success) {
successCount++;
totalResponseTime += responseTime;
}
const successRate = Math.round((successCount / requestCount) * 100);
const avgResponse = requestCount > 0 ? Math.round(totalResponseTime / requestCount) : 0;
const cacheHit = Math.round(Math.random() * 30 + 70); // 模拟缓存命中率
document.getElementById('request-count').textContent = requestCount;
document.getElementById('success-rate').textContent = successRate + '%';
document.getElementById('avg-response').textContent = avgResponse + 'ms';
document.getElementById('cache-hit').textContent = cacheHit + '%';
}
// 页面加载时自动测试健康检查
window.addEventListener('load', function() {
setTimeout(() => {
testAPI('/api/health');
}, 1000);
});
// 添加键盘事件支持
document.getElementById('token-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
setToken();
}
});
// ===== 分块上传功能 =====
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB 固定分片大小
let selectedFile = null;
let fileHash = null;
let totalChunks = 0;
let uploadedChunks = 0;
// 请求池管理
class RequestPool {
constructor(maxConcurrent = 6) {
this.maxConcurrent = maxConcurrent;
this.activePool = new Map(); // 活跃请求池 {requestId: Promise}
this.pendingPool = []; // 待请求池 [{requestId, task, resolve, reject}]
this.requestIdCounter = 0;
}
// 生成请求ID
generateRequestId() {
return ++this.requestIdCounter;
}
// 添加请求到池中
async addRequest(task) {
const requestId = this.generateRequestId();
return new Promise((resolve, reject) => {
const request = {
requestId,
task,
resolve,
reject
};
// 如果活跃池未满,直接执行
if (this.activePool.size < this.maxConcurrent) {
this.executeRequest(request);
} else {
// 否则加入待请求池
this.pendingPool.push(request);
console.log(`请求 ${requestId} 加入待请求池,当前待请求数: ${this.pendingPool.length}`);
}
});
}
// 执行请求
async executeRequest(request) {
const { requestId, task, resolve, reject } = request;
console.log(`开始执行请求 ${requestId},当前活跃请求数: ${this.activePool.size + 1}`);
// 将请求添加到活跃池
const promise = task()
.then(result => {
console.log(`请求 ${requestId} 执行成功`);
resolve(result);
return result;
})
.catch(error => {
console.log(`请求 ${requestId} 执行失败:`, error);
reject(error);
throw error;
})
.finally(() => {
// 请求完成后从活跃池移除
this.activePool.delete(requestId);
console.log(`请求 ${requestId} 完成,从活跃池移除,当前活跃请求数: ${this.activePool.size}`);
// 检查待请求池,如果有待请求则执行
this.processPendingRequests();
// 触发状态更新事件
this.triggerStatusUpdate();
});
this.activePool.set(requestId, promise);
}
// 处理待请求池中的请求
processPendingRequests() {
while (this.pendingPool.length > 0 && this.activePool.size < this.maxConcurrent) {
const request = this.pendingPool.shift();
this.executeRequest(request);
console.log(`从待请求池取出请求 ${request.requestId} 执行,剩余待请求数: ${this.pendingPool.length}`);
}
// 处理完待请求后也触发状态更新
this.triggerStatusUpdate();
}
// 获取池状态
getStatus() {
return {
activeCount: this.activePool.size,
pendingCount: this.pendingPool.length,
maxConcurrent: this.maxConcurrent
};
}
// 清空所有池
clear() {
this.activePool.clear();
this.pendingPool = [];
console.log('请求池已清空');
this.triggerStatusUpdate();
}
// 触发状态更新
triggerStatusUpdate() {
// 使用 setTimeout 确保状态更新在下一个事件循环中执行
setTimeout(() => {
updatePoolStatus();
}, 0);
}
}
// 创建全局请求池实例
const requestPool = new RequestPool(6);
// 文件选择处理
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
// 拖拽处理
const uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.style.borderColor = '#007bff';
uploadArea.style.backgroundColor = '#f8f9fa';
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.style.borderColor = '#ccc';
uploadArea.style.backgroundColor = 'transparent';
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.style.borderColor = '#ccc';
uploadArea.style.backgroundColor = 'transparent';
const files = e.dataTransfer.files;
if (files.length > 0) {
document.getElementById('fileInput').files = files;
handleFileSelect();
}
});
function handleFileSelect() {
const file = document.getElementById('fileInput').files[0];
if (!file) return;
// 重置上传状态
resetUploadState();
selectedFile = file;
totalChunks = Math.ceil(file.size / CHUNK_SIZE);
// 显示文件信息
document.getElementById('fileName').textContent = file.name;
document.getElementById('fileSize').textContent = formatFileSize(file.size);
document.getElementById('chunkCount').textContent = totalChunks;
document.getElementById('chunkSize').textContent = formatFileSize(CHUNK_SIZE);
document.getElementById('fileInfo').style.display = 'block';
document.getElementById('uploadBtn').style.display = 'inline-block';
// 计算文件hash
calculateFileHash(file).then(hash => {
fileHash = hash;
console.log('File hash:', hash);
});
}
// 重置上传状态
function resetUploadState() {
// 重置进度条
document.getElementById('progressFill').style.width = '0%';
document.getElementById('chunkStatus').textContent = '';
document.getElementById('poolStatus').textContent = '';
// 隐藏进度容器
document.getElementById('progressContainer').style.display = 'none';
// 重置状态信息
document.getElementById('uploadStatus').style.display = 'none';
// 重置上传按钮
const uploadBtn = document.getElementById('uploadBtn');
uploadBtn.disabled = false;
uploadBtn.textContent = '开始上传';
// 重置变量
uploadedChunks = 0;
// 清空请求池
requestPool.clear();
}
async function startUpload() {
if (!selectedFile || !fileHash) {
showUploadStatus('请先选择文件', 'error');
return;
}
const uploadBtn = document.getElementById('uploadBtn');
uploadBtn.disabled = true;
uploadBtn.textContent = '上传中...';
document.getElementById('progressContainer').style.display = 'block';
showUploadStatus('开始上传...', 'success');
// 清空之前的请求池
requestPool.clear();
try {
let completedChunks = 0;
let failedChunks = [];
let instantSuccess = false;
let cancelledTasks = false; // 新增:标记是否已取消任务
// 先串行检查第一个分片,检测秒传
console.log('先检查第一个分片,检测秒传...');
const firstChunk = selectedFile.slice(0, CHUNK_SIZE);
const firstChunkHash = await calculateChunkHash(firstChunk);
const firstCheckResult = await checkChunkExists(fileHash, 0, firstChunkHash);
// 如果第一个分片检测到秒传,直接返回
if (firstCheckResult.all_chunk_uploaded) {
console.log('检测到秒传,所有分片都已上传完成!');
updateProgress(totalChunks, totalChunks); // 更新进度到100%
showUploadStatus('秒传成功!文件已完整上传。', 'success');
uploadBtn.textContent = '上传完成';
return;
}
// 如果第一个分片已存在,记录进度
if (firstCheckResult.exists) {
console.log('分片 0 已存在,跳过上传');
completedChunks++;
updateProgress(completedChunks, totalChunks);
} else {
// 第一个分片不存在,需要上传
await uploadChunk(selectedFile, firstChunk, {
fileHash, chunkIndex: 0, totalChunks, chunkHash: firstChunkHash
});
completedChunks++;
updateProgress(completedChunks, totalChunks);
console.log('分片 0 上传完成');
}
// 创建剩余分片的上传任务并添加到请求池(并发处理)
const uploadPromises = [];
for (let i = 1; i < totalChunks; i++) {
const chunkIndex = i;
const promise = requestPool.addRequest(async () => {
try {
const chunk = selectedFile.slice(chunkIndex * CHUNK_SIZE, (chunkIndex + 1) * CHUNK_SIZE);
const chunkHash = await calculateChunkHash(chunk);
// 检查分片是否已存在
const checkResult = await checkChunkExists(fileHash, chunkIndex, chunkHash);
// 如果分片已存在,跳过上传
if (checkResult.exists) {
console.log(`分片 ${chunkIndex} 已存在,跳过上传`);
completedChunks++;
updateProgress(completedChunks, totalChunks);
return { success: true, chunkIndex, skipped: true };
}
// 上传分片
await uploadChunk(selectedFile, chunk, {
fileHash, chunkIndex, totalChunks, chunkHash
});
completedChunks++;
updateProgress(completedChunks, totalChunks);
console.log(`分片 ${chunkIndex} 上传完成`);
return { success: true, chunkIndex };
} catch (error) {
console.error(`分片 ${chunkIndex} 上传失败:`, error);
failedChunks.push({ chunkIndex, error: error.message });
return { success: false, chunkIndex, error: error.message };
}
});
uploadPromises.push(promise);
}
// 等待所有上传任务完成
const results = await Promise.all(uploadPromises);
// 检查是否有秒传成功
if (instantSuccess) {
return; // 秒传成功,直接返回
}
// 检查上传结果
const failedResults = results.filter(result => !result.success);
if (failedResults.length > 0) {
const errorMessage = `部分分片上传失败: ${failedResults.map(f => `分片${f.chunkIndex}(${f.error})`).join(', ')}`;
showUploadStatus(errorMessage, 'error');
uploadBtn.disabled = false;
uploadBtn.textContent = '重新上传';
return;
}
showUploadStatus('上传完成!文件已自动合并。', 'success');
uploadBtn.textContent = '上传完成';
// 确保最终状态显示正确
setTimeout(() => {
updatePoolStatus();
}, 100);
} catch (error) {
console.error('上传失败:', error);
showUploadStatus('上传失败: ' + error.message, 'error');
uploadBtn.disabled = false;
uploadBtn.textContent = '重新上传';
} finally {
// 清空请求池
requestPool.clear();
}
}
// 计算文件hash
async function calculateFileHash(file) {
return new Promise(resolve => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
reader.onload = e => {
spark.append(e.target.result);
resolve(spark.end());
};
reader.readAsArrayBuffer(file);
});
}
// 计算分片hash
async function calculateChunkHash(chunk) {
return new Promise(resolve => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
reader.onload = e => {
spark.append(e.target.result);
resolve(spark.end());
};
reader.readAsArrayBuffer(chunk);
});
}
// 检查分片hash是否已存在
async function checkChunkExists(fileHash, chunkIndex, chunkHash) {
const url = new URL('/upload/chunk_exists', window.location.origin);
url.searchParams.set('file_hash', fileHash);
url.searchParams.set('chunk_index', chunkIndex);
url.searchParams.set('chunk_hash', chunkHash);
url.searchParams.set('file_size', selectedFile.size);
url.searchParams.set('trunk_size', CHUNK_SIZE);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const res = await fetch(url.toString());
if (!res.ok) return { exists: false, all_chunk_uploaded: false };
const data = await res.json();
// 如果所有分片都已上传完成,直接返回 true 表示秒传成功
if (data.all_chunk_uploaded) {
console.log('所有分片都已上传完成,秒传成功!');
return { exists: true, all_chunk_uploaded: true };
}
return { exists: data.exists, all_chunk_uploaded: false };
}
// 上传分片
async function uploadChunk(file, chunk, { fileHash, chunkIndex, totalChunks, chunkHash }) {
const form = new FormData();
form.append('file_hash', fileHash);
form.append('chunk_index', chunkIndex);
form.append('total_chunks', totalChunks);
form.append('filename', file.name);
form.append('file_size', file.size);
form.append('chunk_size', CHUNK_SIZE);
form.append('chunk_hash', chunkHash);
form.append('chunk', chunk, file.name + '.part' + chunkIndex);
const url = new URL('/upload/chunk', window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString(), {
method: 'POST',
body: form
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '上传失败');
}
const result = await response.json();
if (result.all_chunk_uploaded) {
console.log('所有分片已上传完成,文件已自动合并:', result.file_path);
}
}
// 更新进度
function updateProgress(current, total) {
const percentage = (current / total) * 100;
document.getElementById('progressFill').style.width = percentage + '%';
document.getElementById('chunkStatus').textContent = current + '/' + total + ' 分片已上传 (' + percentage.toFixed(1) + '%)';
// 更新请求池状态
updatePoolStatus();
}
// 更新请求池状态显示
function updatePoolStatus() {
const status = requestPool.getStatus();
const poolStatusElement = document.getElementById('poolStatus');
if (poolStatusElement) {
// 如果所有请求都完成了,显示完成状态
if (status.activeCount === 0 && status.pendingCount === 0) {
poolStatusElement.textContent = `请求池状态: 所有请求已完成`;
} else {
poolStatusElement.textContent = `请求池状态: ${status.activeCount}/${status.maxConcurrent} 活跃, ${status.pendingCount} 等待中`;
}
}
}
// 显示上传状态
function showUploadStatus(message, type) {
const status = document.getElementById('uploadStatus');
status.textContent = message;
status.className = 'status ' + type;
status.style.display = 'block';
}
// 格式化文件大小
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];
}
</script>
</body>
</html>