Files
JieXi/templates/profile.html
2025-11-30 19:49:25 +08:00

815 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>个人中心 - 短视频解析平台</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;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: var(--shadow-md);
}
.stat-label {
font-size: 0.875rem;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--secondary-900);
}
.stat-value.primary {
color: var(--primary-600);
}
.stat-value.success {
color: var(--success);
}
.stat-value.warning {
color: var(--warning);
}
.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);
}
.info-row {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid var(--secondary-100);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: var(--text-muted);
font-size: 0.875rem;
}
.info-value {
font-weight: 500;
color: var(--secondary-800);
}
.group-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.group-badge.normal {
background: var(--secondary-100);
color: var(--secondary-700);
}
.group-badge.vip {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: white;
}
.group-badge.svip {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
color: white;
}
.logs-card {
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.logs-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.logs-header h3 {
font-size: 1.125rem;
font-weight: 600;
color: var(--secondary-900);
margin: 0;
}
.logs-table {
width: 100%;
border-collapse: collapse;
}
.logs-table th,
.logs-table td {
padding: 0.875rem 1rem;
text-align: left;
font-size: 0.875rem;
}
.logs-table th {
background: var(--secondary-50);
color: var(--text-muted);
font-weight: 500;
}
.logs-table tr:not(:last-child) td {
border-bottom: 1px solid var(--secondary-100);
}
.logs-table .platform {
font-weight: 500;
color: var(--secondary-800);
}
.logs-table .url {
color: var(--primary-600);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: color 0.2s;
}
.logs-table .url:hover {
color: var(--primary-700);
text-decoration: underline;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 500;
}
.status-badge.success {
background: #dcfce7;
color: #166534;
}
.status-badge.failed {
background: #fee2e2;
color: #991b1b;
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-muted);
}
.progress-bar {
height: 8px;
background: var(--secondary-100);
border-radius: 4px;
overflow: hidden;
margin-top: 0.5rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
border-radius: 4px;
transition: width 0.3s ease;
}
@media (max-width: 640px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
.logs-table {
display: block;
overflow-x: auto;
}
}
/* API Key 管理样式 */
.api-key-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--secondary-100);
border-radius: var(--radius-md);
margin-bottom: 0.5rem;
}
.api-key-item:last-child {
margin-bottom: 0;
}
.api-key-info {
flex: 1;
}
.api-key-name {
font-weight: 500;
color: var(--secondary-800);
margin-bottom: 0.25rem;
}
.api-key-meta {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.api-key-status {
padding: 0.125rem 0.375rem;
border-radius: var(--radius-sm);
font-weight: 500;
}
.api-key-status.active {
background: #dcfce7;
color: #166534;
}
.api-key-status.inactive {
background: #fee2e2;
color: #991b1b;
}
.api-key-actions {
display: flex;
gap: 0.5rem;
}
/* 弹窗样式 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
border-radius: var(--radius-lg);
width: 100%;
max-width: 400px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h3 {
margin: 0;
font-size: 1.125rem;
border: none;
padding: 0;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-muted);
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--secondary-700);
}
</style>
</head>
<body>
<div class="container">
<div class="page-header">
<h1 class="page-title">个人中心</h1>
<div class="header-actions">
<a href="/" class="ui-btn ui-btn-secondary">返回首页</a>
<button class="ui-btn ui-btn-danger" onclick="logout()">退出登录</button>
</div>
</div>
<!-- 使用统计 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">今日已用</div>
<div class="stat-value primary" id="todayUsed">-</div>
</div>
<div class="stat-card">
<div class="stat-label">今日剩余</div>
<div class="stat-value success" id="todayRemaining">-</div>
</div>
<div class="stat-card">
<div class="stat-label">每日限额</div>
<div class="stat-value" id="dailyLimit">-</div>
</div>
<div class="stat-card">
<div class="stat-label">累计解析</div>
<div class="stat-value" id="totalCount">-</div>
</div>
</div>
<!-- 用户信息 -->
<div class="info-card">
<h3>账户信息</h3>
<div class="info-row">
<span class="info-label">用户名</span>
<span class="info-value" id="username">-</span>
</div>
<div class="info-row">
<span class="info-label">邮箱</span>
<span class="info-value" id="email">-</span>
</div>
<div class="info-row">
<span class="info-label">用户组</span>
<span class="info-value" id="groupName">-</span>
</div>
<div class="info-row" id="expiryRow" style="display: none;">
<span class="info-label">套餐到期</span>
<span class="info-value" id="expiryTime">-</span>
</div>
<div class="info-row">
<span class="info-label">注册时间</span>
<span class="info-value" id="createdAt">-</span>
</div>
<div class="info-row">
<span class="info-label">今日使用进度</span>
<span class="info-value" id="usageText">-</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="usageProgress" style="width: 0%"></div>
</div>
</div>
<!-- 兑换码 -->
<div class="info-card">
<h3>兑换码</h3>
<p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1rem;">
输入兑换码升级您的账户套餐
</p>
<div style="display: flex; gap: 0.75rem;">
<input type="text" id="redeemCode" class="ui-input" placeholder="请输入兑换码" style="flex: 1; text-transform: uppercase;">
<button class="ui-btn ui-btn-primary" onclick="redeemCode()">兑换</button>
</div>
</div>
<!-- API Key 管理 -->
<div class="info-card">
<h3 style="display: flex; justify-content: space-between; align-items: center;">
API Key 管理
<button class="ui-btn ui-btn-primary ui-btn-sm" onclick="showCreateKeyModal()">创建 Key</button>
</h3>
<p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1rem;">
通过 API Key 可以在您的应用中调用视频解析接口最多5个
</p>
<div id="apiKeyList">
<p style="color: var(--text-muted); text-align: center;">加载中...</p>
</div>
</div>
<!-- 创建 API Key 弹窗 -->
<div class="modal" id="createKeyModal">
<div class="modal-content">
<div class="modal-header">
<h3>创建 API Key</h3>
<button class="modal-close" onclick="closeModal('createKeyModal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Key 名称</label>
<input type="text" id="keyName" class="ui-input" placeholder="如:我的应用">
</div>
</div>
<div class="modal-footer">
<button class="ui-btn ui-btn-secondary" onclick="closeModal('createKeyModal')">取消</button>
<button class="ui-btn ui-btn-primary" onclick="createApiKey()">创建</button>
</div>
</div>
</div>
<!-- 显示新 Key 弹窗 -->
<div class="modal" id="newKeyModal">
<div class="modal-content">
<div class="modal-header">
<h3>API Key 创建成功</h3>
<button class="modal-close" onclick="closeModal('newKeyModal')">&times;</button>
</div>
<div class="modal-body">
<p style="color: #dc2626; font-size: 0.875rem; margin-bottom: 1rem;">
请立即复制保存,此 Key 只显示一次!
</p>
<div class="form-group">
<label>API Key</label>
<input type="text" id="newKeyValue" class="ui-input" readonly style="font-family: monospace;">
</div>
</div>
<div class="modal-footer">
<button class="ui-btn ui-btn-primary" onclick="copyNewKey()">复制 Key</button>
</div>
</div>
</div>
<!-- 解析记录 -->
<div class="logs-card">
<div class="logs-header">
<h3>解析记录最近20条</h3>
</div>
<table class="logs-table">
<thead>
<tr>
<th>平台</th>
<th>视频链接</th>
<th>状态</th>
<th>耗时</th>
<th>时间</th>
</tr>
</thead>
<tbody id="logsBody">
<tr>
<td colspan="5" class="empty-state">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<script src="/static/js/ui-components.js"></script>
<script>
async function loadProfile() {
try {
const response = await fetch('/auth/api/profile');
const result = await response.json();
if (!result.success) {
if (response.status === 401) {
window.location.href = '/auth/login';
return;
}
throw new Error(result.message || '加载失败');
}
const data = result.data;
// 更新统计数据
document.getElementById('todayUsed').textContent = data.usage.today_used;
document.getElementById('todayRemaining').textContent = data.usage.today_remaining;
document.getElementById('dailyLimit').textContent = data.usage.daily_limit;
document.getElementById('totalCount').textContent = data.usage.total_parse_count;
// 更新用户信息
document.getElementById('username').textContent = data.user.username;
document.getElementById('email').textContent = data.user.email;
document.getElementById('createdAt').textContent = data.user.created_at || '-';
// 用户组徽章
const groupBadge = getGroupBadge(data.group.name);
document.getElementById('groupName').innerHTML = groupBadge;
// 显示套餐到期时间(在账户信息卡片中)
if (data.group.expires_at) {
document.getElementById('expiryRow').style.display = 'flex';
const expiryText = data.group.is_expired
? `<span style="color: #dc2626;">${data.group.expires_at}(已过期)</span>`
: data.group.expires_at;
document.getElementById('expiryTime').innerHTML = expiryText;
}
// 使用进度
const usagePercent = data.usage.daily_limit > 0
? Math.round((data.usage.today_used / data.usage.daily_limit) * 100)
: 0;
document.getElementById('usageText').textContent = `${data.usage.today_used} / ${data.usage.daily_limit}`;
document.getElementById('usageProgress').style.width = `${Math.min(usagePercent, 100)}%`;
// 更新解析记录
const logsBody = document.getElementById('logsBody');
if (data.parse_logs.length === 0) {
logsBody.innerHTML = '<tr><td colspan="5" class="empty-state">暂无解析记录</td></tr>';
} else {
logsBody.innerHTML = data.parse_logs.map((log, index) => `
<tr>
<td class="platform">${getPlatformName(log.platform)}</td>
<td class="url" title="点击复制链接" data-url="${log.video_url}" onclick="copyUrlFromElement(this)">${log.video_url}</td>
<td><span class="status-badge ${log.status === 'success' ? 'success' : 'failed'}">${log.status === 'success' ? '成功' : '失败'}</span></td>
<td>${log.response_time ? (log.response_time / 1000).toFixed(2) + 's' : '-'}</td>
<td>${log.created_at}</td>
</tr>
`).join('');
}
} catch (error) {
UI.notify('加载失败: ' + error.message, 'error');
}
}
function getGroupBadge(groupName) {
const name = groupName.toLowerCase();
if (name.includes('svip')) {
return `<span class="group-badge svip">${groupName}</span>`;
} else if (name.includes('vip')) {
return `<span class="group-badge vip">${groupName}</span>`;
}
return `<span class="group-badge normal">${groupName}</span>`;
}
function getPlatformName(platform) {
const names = {
'douyin': '抖音',
'tiktok': 'TikTok',
'bilibili': 'B站'
};
return names[platform] || platform;
}
function copyUrlFromElement(element) {
const url = element.getAttribute('data-url');
if (!url) {
UI.notify('无法获取链接', 'error');
return;
}
navigator.clipboard.writeText(url).then(() => {
UI.notify('链接已复制', 'success');
}).catch(() => {
UI.notify('复制失败', 'error');
});
}
async function logout() {
try {
await fetch('/auth/logout', { method: 'POST' });
UI.notify('已退出登录', 'success');
setTimeout(() => {
window.location.href = '/';
}, 1000);
} catch (error) {
UI.notify('退出失败', 'error');
}
}
async function redeemCode() {
const code = document.getElementById('redeemCode').value.trim();
if (!code) {
UI.notify('请输入兑换码', 'warning');
return;
}
try {
const response = await fetch('/auth/api/redeem', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: code })
});
const result = await response.json();
if (result.success) {
UI.notify(result.message, 'success');
document.getElementById('redeemCode').value = '';
loadProfile();
} else {
UI.notify(result.message, 'error');
}
} catch (error) {
UI.notify('兑换失败', 'error');
}
}
document.getElementById('redeemCode').addEventListener('keypress', (e) => {
if (e.key === 'Enter') redeemCode();
});
// ==================== API Key 管理 ====================
async function loadApiKeys() {
try {
const response = await fetch('/user/apikey/list');
const result = await response.json();
const container = document.getElementById('apiKeyList');
if (!result.success) {
container.innerHTML = '<p style="color: var(--text-muted); text-align: center;">加载失败</p>';
return;
}
if (result.data.length === 0) {
container.innerHTML = '<p style="color: var(--text-muted); text-align: center;">暂无 API Key</p>';
return;
}
container.innerHTML = result.data.map(key => `
<div class="api-key-item">
<div class="api-key-info">
<div class="api-key-name">${key.name || '未命名'}</div>
<div class="api-key-meta">
<span>Key: ${key.api_key}</span>
<span>调用: ${key.total_calls || 0}次</span>
<span class="api-key-status ${key.is_active ? 'active' : 'inactive'}">${key.is_active ? '启用' : '禁用'}</span>
</div>
</div>
<div class="api-key-actions">
<button class="ui-btn ui-btn-sm ${key.is_active ? 'ui-btn-secondary' : 'ui-btn-primary'}" onclick="toggleApiKey(${key.id})">
${key.is_active ? '禁用' : '启用'}
</button>
<button class="ui-btn ui-btn-sm ui-btn-danger" onclick="deleteApiKey(${key.id})">删除</button>
</div>
</div>
`).join('');
} catch (error) {
document.getElementById('apiKeyList').innerHTML = '<p style="color: var(--text-muted); text-align: center;">加载失败</p>';
}
}
function showCreateKeyModal() {
document.getElementById('keyName').value = '';
document.getElementById('createKeyModal').classList.add('show');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('show');
}
async function createApiKey() {
const name = document.getElementById('keyName').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: name })
});
const result = await response.json();
if (result.success) {
closeModal('createKeyModal');
document.getElementById('newKeyValue').value = result.data.api_key;
document.getElementById('newKeyModal').classList.add('show');
loadApiKeys();
} else {
UI.notify(result.message, 'error');
}
} catch (error) {
UI.notify('创建失败', 'error');
}
}
function copyNewKey() {
const input = document.getElementById('newKeyValue');
input.select();
document.execCommand('copy');
UI.notify('已复制到剪贴板', 'success');
}
async function toggleApiKey(keyId) {
try {
const response = await fetch(`/user/apikey/toggle/${keyId}`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
UI.notify(result.message, 'success');
loadApiKeys();
} else {
UI.notify(result.message, 'error');
}
} catch (error) {
UI.notify('操作失败', 'error');
}
}
async function deleteApiKey(keyId) {
if (!confirm('确定要删除这个 API Key 吗?')) return;
try {
const response = await fetch(`/user/apikey/delete/${keyId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
UI.notify('删除成功', 'success');
loadApiKeys();
} else {
UI.notify(result.message, 'error');
}
} catch (error) {
UI.notify('删除失败', 'error');
}
}
loadProfile();
loadApiKeys();
</script>
</body>
</html>