466 lines
14 KiB
HTML
466 lines
14 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 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>
|