feat: 新增平台

This commit is contained in:
2025-11-30 19:49:25 +08:00
parent c3e56a954d
commit fbd2c491b2
41 changed files with 4293 additions and 76 deletions

View File

@@ -0,0 +1,465 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Key 管理 - 短视频解析平台</title>
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
<style>
body {
background: linear-gradient(135deg, #f0f9ff 0%, #e0e7ff 100%);
min-height: 100vh;
padding: 2rem 1rem;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 1.75rem;
font-weight: 700;
color: var(--secondary-900);
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.info-card {
background: white;
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: var(--shadow-md);
margin-bottom: 2rem;
}
.info-card h3 {
font-size: 1.125rem;
font-weight: 600;
color: var(--secondary-900);
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.key-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.key-item {
background: var(--secondary-50);
border-radius: var(--radius-md);
padding: 1.25rem;
border: 1px solid var(--border-color);
}
.key-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.key-name {
font-weight: 600;
color: var(--secondary-900);
font-size: 1rem;
}
.key-status {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 500;
}
.key-status.active {
background: #dcfce7;
color: #166534;
}
.key-status.inactive {
background: #fee2e2;
color: #991b1b;
}
.key-value {
font-family: monospace;
background: white;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-sm);
font-size: 0.875rem;
color: var(--secondary-700);
margin-bottom: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--border-color);
}
.key-meta {
display: flex;
gap: 1.5rem;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.key-actions {
display: flex;
gap: 0.5rem;
}
.key-actions button {
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-muted);
}
.create-form {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.create-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: 0.9rem;
}
.create-form input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px var(--primary-100);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.modal-overlay.show {
opacity: 1;
visibility: visible;
}
.modal-content {
background: white;
border-radius: var(--radius-lg);
padding: 2rem;
max-width: 500px;
width: 90%;
box-shadow: var(--shadow-xl);
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--secondary-900);
}
.modal-key {
background: var(--secondary-50);
padding: 1rem;
border-radius: var(--radius-md);
font-family: monospace;
font-size: 0.9rem;
word-break: break-all;
margin-bottom: 1rem;
border: 1px solid var(--border-color);
}
.modal-warning {
background: #fef3c7;
color: #92400e;
padding: 0.75rem 1rem;
border-radius: var(--radius-md);
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
.tip-box {
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: var(--radius-md);
padding: 1rem;
margin-bottom: 1.5rem;
font-size: 0.875rem;
color: #1e40af;
}
@media (max-width: 640px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.create-form {
flex-direction: column;
}
.key-meta {
flex-direction: column;
gap: 0.5rem;
}
.key-actions {
flex-wrap: wrap;
}
}
</style>
</head>
<body>
<div class="container">
<div class="page-header">
<h1 class="page-title">API Key 管理</h1>
<div class="header-actions">
<a href="/api-docs" class="ui-btn ui-btn-secondary">API 文档</a>
<a href="/auth/profile" class="ui-btn ui-btn-secondary">个人中心</a>
<a href="/" class="ui-btn ui-btn-secondary">返回首页</a>
</div>
</div>
<div class="info-card">
<h3>创建新的 API Key</h3>
<div class="tip-box">
API Key 用于调用解析接口,请妥善保管。每个用户最多创建 5 个 Key。
</div>
<div class="create-form">
<input type="text" id="keyName" placeholder="输入 Key 名称(如:我的应用)" maxlength="100">
<button class="ui-btn ui-btn-primary" onclick="createKey()">创建 Key</button>
</div>
</div>
<div class="info-card">
<h3>我的 API Keys</h3>
<div class="key-list" id="keyList">
<div class="empty-state">加载中...</div>
</div>
</div>
</div>
<!-- 新建成功弹窗 -->
<div class="modal-overlay" id="keyModal">
<div class="modal-content">
<h3 class="modal-title">API Key 创建成功</h3>
<div class="modal-key" id="newKeyValue"></div>
<div class="modal-warning">
请立即复制并保存此 Key关闭后将无法再次查看完整内容
</div>
<div class="modal-actions">
<button class="ui-btn ui-btn-secondary" onclick="copyNewKey()">复制 Key</button>
<button class="ui-btn ui-btn-primary" onclick="closeModal()">我已保存</button>
</div>
</div>
</div>
<script src="/static/js/ui-components.js"></script>
<script>
let newApiKey = '';
async function loadKeys() {
try {
const response = await fetch('/user/apikey/list');
const result = await response.json();
if (!result.success) {
if (response.status === 401) {
window.location.href = '/auth/login';
return;
}
throw new Error(result.message || '加载失败');
}
renderKeys(result.data);
} catch (error) {
UI.notify('加载失败: ' + error.message, 'error');
}
}
function renderKeys(keys) {
const container = document.getElementById('keyList');
if (keys.length === 0) {
container.innerHTML = '<div class="empty-state">暂无 API Key请创建一个</div>';
return;
}
container.innerHTML = keys.map(key => `
<div class="key-item">
<div class="key-header">
<span class="key-name">${escapeHtml(key.name)}</span>
<span class="key-status ${key.is_active ? 'active' : 'inactive'}">
${key.is_active ? '启用' : '禁用'}
</span>
</div>
<div class="key-value">
<span>${key.api_key}</span>
</div>
<div class="key-meta">
<span>每日限额: ${key.daily_limit} 次</span>
<span>今日已用: ${key.today_calls} 次</span>
<span>总调用: ${key.total_calls} 次</span>
<span>创建时间: ${key.created_at}</span>
</div>
<div class="key-actions">
<button class="ui-btn ui-btn-secondary" onclick="toggleKey(${key.id}, ${key.is_active})">
${key.is_active ? '禁用' : '启用'}
</button>
<button class="ui-btn ui-btn-danger" onclick="deleteKey(${key.id}, '${escapeHtml(key.name)}')">
删除
</button>
</div>
</div>
`).join('');
}
async function createKey() {
const nameInput = document.getElementById('keyName');
const name = nameInput.value.trim();
if (!name) {
UI.notify('请输入 Key 名称', 'warning');
return;
}
try {
const response = await fetch('/user/apikey/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '创建失败');
}
// 显示新 Key
newApiKey = result.data.api_key;
document.getElementById('newKeyValue').textContent = newApiKey;
document.getElementById('keyModal').classList.add('show');
nameInput.value = '';
loadKeys();
} catch (error) {
UI.notify(error.message, 'error');
}
}
async function toggleKey(keyId, currentStatus) {
try {
const response = await fetch(`/user/apikey/toggle/${keyId}`, {
method: 'POST'
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '操作失败');
}
UI.notify(result.message, 'success');
loadKeys();
} catch (error) {
UI.notify(error.message, 'error');
}
}
async function deleteKey(keyId, keyName) {
if (!confirm(`确定要删除 "${keyName}" 吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/user/apikey/delete/${keyId}`, {
method: 'DELETE'
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '删除失败');
}
UI.notify('删除成功', 'success');
loadKeys();
} catch (error) {
UI.notify(error.message, 'error');
}
}
function copyNewKey() {
navigator.clipboard.writeText(newApiKey).then(() => {
UI.notify('已复制到剪贴板', 'success');
}).catch(() => {
UI.notify('复制失败,请手动复制', 'error');
});
}
function closeModal() {
document.getElementById('keyModal').classList.remove('show');
newApiKey = '';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 点击遮罩关闭
document.getElementById('keyModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
loadKeys();
</script>
</body>
</html>