238 lines
11 KiB
HTML
238 lines
11 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>API测试工具</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f5f7fa; padding: 20px; }
|
|
.container { max-width: 1000px; margin: 0 auto; }
|
|
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
|
.header h1 { font-size: 24px; font-weight: 600; }
|
|
.header p { font-size: 14px; opacity: 0.9; margin-top: 5px; }
|
|
|
|
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
|
.card-title { font-size: 16px; font-weight: 600; color: #333; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; }
|
|
|
|
.form-group { margin-bottom: 15px; }
|
|
.form-group label { display: block; font-size: 14px; color: #555; margin-bottom: 6px; }
|
|
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; transition: border-color 0.2s; }
|
|
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: #667eea; }
|
|
.form-group textarea { resize: vertical; min-height: 80px; }
|
|
|
|
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; }
|
|
|
|
.btn { display: inline-block; padding: 10px 24px; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
|
|
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
|
|
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); }
|
|
.btn-success { background: #28a745; color: white; }
|
|
.btn-success:hover { background: #218838; }
|
|
.btn-group { display: flex; gap: 10px; }
|
|
|
|
.result-area { margin-top: 15px; }
|
|
.result-area pre { background: #f8f9fa; border-radius: 4px; padding: 15px; font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; white-space: pre-wrap; word-break: break-all; max-height: 500px; overflow-y: auto; color: #333; }
|
|
|
|
.signature-box { background: #e8f5e9; border: 1px solid #c8e6c9; border-radius: 4px; padding: 15px; margin-top: 10px; }
|
|
.signature-box .label { font-size: 12px; color: #2e7d32; margin-bottom: 8px; }
|
|
.signature-box .value { font-family: 'Consolas', monospace; font-size: 13px; color: #1b5e20; word-break: break-all; }
|
|
|
|
.endpoint-tabs { display: flex; gap: 8px; margin-bottom: 15px; }
|
|
.endpoint-tabs button { padding: 8px 16px; border: 1px solid #ddd; background: white; border-radius: 4px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
|
|
.endpoint-tabs button.active { background: #667eea; color: white; border-color: #667eea; }
|
|
|
|
.loading { display: inline-block; width: 20px; height: 20px; border: 2px solid #f3f3f3; border-top: 2px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; }
|
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>API测试工具</h1>
|
|
<p>签名算法: HMAC-SHA256 | 支持出库(chuku)和入库(ruku)接口</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">1. 配置API密钥</div>
|
|
<div class="grid">
|
|
<div class="form-group">
|
|
<label>API Key</label>
|
|
<input type="text" id="apiKey" value="" placeholder="输入API Key">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>API Secret</label>
|
|
<input type="text" id="apiSecret" value="" placeholder="输入API Secret">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">2. 请求参数</div>
|
|
<div class="endpoint-tabs">
|
|
<button class="active" onclick="switchEndpoint('chuku')">出库接口 /api/v1/chuku</button>
|
|
<button onclick="switchEndpoint('ruku')">入库接口 /api/v1/ruku</button>
|
|
</div>
|
|
<div class="grid">
|
|
<div class="form-group">
|
|
<label>开始日期 (start_date)</label>
|
|
<input type="date" id="startDate" value="2024-02-02">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>结束日期 (end_date)</label>
|
|
<input type="date" id="endDate" value="2026-06-22">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>页码 (page)</label>
|
|
<input type="number" id="page" value="1" min="1">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>每页数量 (limit)</label>
|
|
<input type="number" id="limit" value="100" min="1" max="500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">3. 签名生成</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-primary" onclick="generateSignature()">生成签名</button>
|
|
<button class="btn btn-success" onclick="executeQuery()">执行查询</button>
|
|
</div>
|
|
<div class="signature-box" id="signatureBox" style="display: none;">
|
|
<div class="label">生成的请求信息:</div>
|
|
<div class="value" id="signatureInfo"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">4. 查询结果</div>
|
|
<div id="resultArea"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentEndpoint = 'chuku';
|
|
|
|
function switchEndpoint(endpoint) {
|
|
currentEndpoint = endpoint;
|
|
document.querySelectorAll('.endpoint-tabs button').forEach(btn => btn.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
async function generateSignature() {
|
|
const apiKey = document.getElementById('apiKey').value;
|
|
const apiSecret = document.getElementById('apiSecret').value;
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
const page = document.getElementById('page').value;
|
|
const limit = document.getElementById('limit').value;
|
|
|
|
const params = {
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
page: page,
|
|
limit: limit
|
|
};
|
|
|
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
|
|
const sortedKeys = Object.keys(params).sort();
|
|
let paramStr = '';
|
|
for (let i = 0; i < sortedKeys.length; i++) {
|
|
const k = sortedKeys[i];
|
|
const v = params[k];
|
|
if (v !== null && v !== '') {
|
|
if (paramStr) paramStr += '&';
|
|
paramStr += k + '=' + v;
|
|
}
|
|
}
|
|
const fullStr = paramStr + '×tamp=' + timestamp;
|
|
|
|
const sign = await hmacSha256(fullStr, apiSecret);
|
|
|
|
const url = `http://localhost/api/v1/${currentEndpoint}?${paramStr}`;
|
|
|
|
const info = `
|
|
请求URL: ${url}
|
|
|
|
Headers:
|
|
X-API-Key: ${apiKey}
|
|
X-Timestamp: ${timestamp}
|
|
X-Sign: ${sign}
|
|
|
|
签名字符串:
|
|
${fullStr}
|
|
|
|
签名有效期: 7天 (当前时间戳: ${new Date(parseInt(timestamp) * 1000).toLocaleString()})
|
|
`.trim();
|
|
|
|
document.getElementById('signatureInfo').textContent = info;
|
|
document.getElementById('signatureBox').style.display = 'block';
|
|
|
|
window.signatureCache = {
|
|
url: url,
|
|
headers: {
|
|
'X-API-Key': apiKey,
|
|
'X-Timestamp': timestamp,
|
|
'X-Sign': sign
|
|
}
|
|
};
|
|
}
|
|
|
|
async function executeQuery() {
|
|
if (!window.signatureCache) {
|
|
await generateSignature();
|
|
}
|
|
|
|
const resultArea = document.getElementById('resultArea');
|
|
resultArea.innerHTML = '<div style="text-align:center; padding:20px;"><div class="loading"></div> 查询中...</div>';
|
|
|
|
try {
|
|
const response = await fetch(window.signatureCache.url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': window.signatureCache.headers['X-API-Key'],
|
|
'X-Timestamp': window.signatureCache.headers['X-Timestamp'],
|
|
'X-Sign': window.signatureCache.headers['X-Sign']
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
let html = '<pre>' + JSON.stringify(result, null, 2) + '</pre>';
|
|
|
|
if (result.code === 200) {
|
|
html = '<div style="color:#28a745; margin-bottom:10px;">✅ 查询成功</div>' + html;
|
|
} else {
|
|
html = '<div style="color:#dc3545; margin-bottom:10px;">❌ 查询失败: ' + (result.msg || '未知错误') + '</div>' + html;
|
|
}
|
|
|
|
resultArea.innerHTML = html;
|
|
} catch (error) {
|
|
resultArea.innerHTML = '<div style="color:#dc3545; margin-bottom:10px;">❌ 请求失败: ' + error.message + '</div><pre>' + error.stack + '</pre>';
|
|
}
|
|
}
|
|
|
|
function hmacSha256(message, secret) {
|
|
const encoder = new TextEncoder();
|
|
const messageData = encoder.encode(message);
|
|
const secretData = encoder.encode(secret);
|
|
|
|
return crypto.subtle.importKey(
|
|
'raw',
|
|
secretData,
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['sign']
|
|
).then(key => {
|
|
return crypto.subtle.sign('HMAC', key, messageData);
|
|
}).then(signature => {
|
|
return Array.from(new Uint8Array(signature))
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |