Infini / templates /account_statements.html
yangtb24's picture
Upload 7 files
931e053 verified
<!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="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #ffffff;
--text-color: #111827;
--secondary-text-color: #6b7280;
--border-color: #e5e7eb;
--card-bg-color: #ffffff;
--accent-color: #000000;
--error-color: #ef4444;
--success-color: #10b981;
--info-color: #3b82f6;
--income-color: #22c55e;
--expense-color: #ef4444;
--summary-bar-bg: #f9fafb;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
background-color: var(--summary-bar-bg);
color: var(--text-color);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 32px 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border-color);
}
header h1 {
margin: 0;
font-size: 28px;
color: var(--text-color);
font-weight: 700;
}
.header-actions {
display: flex;
align-items: center;
}
.header-actions .back-link {
padding: 8px 16px;
background-color: var(--bg-color);
color: var(--secondary-text-color);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.header-actions .back-link:hover {
background-color: #f3f4f6;
border-color: #d1d5db;
text-decoration: none;
}
#account-summary-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
background-color: var(--card-bg-color);
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px solid var(--border-color);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.summary-item {
text-align: left;
}
.summary-label {
display: block;
font-size: 0.875em;
color: var(--secondary-text-color);
margin-bottom: 4px;
font-weight: 500;
}
.summary-value {
display: block;
font-size: 1.5em;
color: var(--text-color);
font-weight: 600;
}
.statement-card-item {
background-color: var(--card-bg-color);
border-radius: 8px;
border: 1px solid var(--border-color);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
cursor: pointer;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.statement-card-item:hover {
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
transform: translateY(-2px);
}
.statement-card-item .card-summary {
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.statement-card-item .summary-left { display: flex; align-items: center; gap: 12px; }
.statement-card-item .change-type-icon {
width: 32px; height: 32px; border-radius: 50%; display: flex;
align-items: center; justify-content: center; font-weight: 600; color: white;
}
.statement-card-item .summary-info .description { font-weight: 500; color: var(--text-color); }
.statement-card-item .summary-info .timestamp { font-size: 0.875em; color: var(--secondary-text-color); }
.statement-card-item .summary-right .amount { font-weight: 600; font-size: 1.1em; }
.statement-card-item .amount.positive { color: var(--income-color); }
.statement-card-item .amount.negative { color: var(--expense-color); }
.statement-card-item .amount.neutral { color: var(--text-color); }
.statement-card-item .card-details {
padding: 0 20px 16px 20px;
border-top: 1px solid var(--border-color);
margin-top: 12px;
display: none;
}
.statement-card-item .details-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px 20px; margin-top: 12px;
}
.statement-card-item .detail-item { font-size: 0.9em; }
.statement-card-item .detail-item strong { color: var(--secondary-text-color); font-weight: 500; margin-right: 8px; display: inline-block; min-width: 80px;}
.statement-card-item .detail-item span { color: var(--text-color); word-break: break-all; }
.statement-card-item .metadata-title { font-weight: 600; margin-top: 16px; margin-bottom: 8px; font-size: 0.95em; color: var(--text-color); }
.statement-card-item .metadata-item { font-size: 0.85em; padding-left: 10px; border-left: 2px solid var(--border-color); margin-bottom: 4px; }
.statement-card-item .metadata-item strong { min-width: 70px; }
#statements-loading-indicator {
text-align: center; padding: 40px; font-size: 1em; color: var(--secondary-text-color); display: none; width: 100%;
}
#statements-loading-indicator::before {
content: ''; display: inline-block; width: 24px; height: 24px;
border: 3px solid rgba(0,0,0, 0.1); border-radius: 50%;
border-top-color: var(--text-color); animation: spin 0.8s linear infinite;
margin-right: 12px; position: relative; top: 4px;
}
@keyframes spin { to { transform: rotate(360deg); } }
#pagination-controls {
margin-top: 20px;
text-align: center;
}
#pagination-controls button {
padding: 8px 16px;
margin: 0 5px;
border: 1px solid var(--border-color);
background-color: var(--card-bg-color);
color: var(--text-color);
border-radius: 6px;
cursor: pointer;
}
#pagination-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#pagination-controls button:hover:not(:disabled) {
border-color: var(--accent-color);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>账户账单详情</h1>
<div class="header-actions">
<a href="{{ url_for('dashboard') }}" class="back-link">&larr; 返回仪表盘</a>
</div>
</header>
<div id="account-summary-bar">
<div class="summary-item">
<span class="summary-label">总收入</span>
<span class="summary-value" id="summary-total-income">--.-- USD</span>
</div>
<div class="summary-item">
<span class="summary-label">总支出</span>
<span class="summary-value" id="summary-total-expense">--.-- USD</span>
</div>
<div class="summary-item">
<span class="summary-label">总结余</span>
<span class="summary-value" id="summary-total-balance">--.-- USD</span>
</div>
</div>
<div id="statements-loading-indicator">正在加载账单...</div>
<div id="statement-records-container">
</div>
<div id="pagination-controls">
<button id="prev-page-btn" disabled>上一页</button>
<span id="page-info">第 1 页</span>
<button id="next-page-btn" disabled>下一页</button>
</div>
</div>
<script>
const accountEmail = "{{ account_email }}";
const recordsContainer = document.getElementById('statement-records-container');
const loadingIndicator = document.getElementById('statements-loading-indicator');
const prevPageBtn = document.getElementById('prev-page-btn');
const nextPageBtn = document.getElementById('next-page-btn');
const pageInfoElem = document.getElementById('page-info');
const summaryTotalIncomeElem = document.getElementById('summary-total-income');
const summaryTotalExpenseElem = document.getElementById('summary-total-expense');
const summaryTotalBalanceElem = document.getElementById('summary-total-balance');
let currentPage = 1;
const pageSize = 20;
let totalRecords = 0;
function formatStatementTimestamp(unixTimestamp) {
if (!unixTimestamp) return 'N/A';
const date = new Date(unixTimestamp * 1000);
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
}
function getChangeTypeDetails(type) {
const types = {
1: { text: "充值", icon: "➕", color: "#22c55e" }, 2: { text: "提现", icon: "➖", color: "#ef4444" },
3: { text: "接收", icon: "➡️", color: "#3b82f6" }, 4: { text: "发送", icon: "⬅️", color: "#f97316" },
5: { text: "消费", icon: "💳", color: "#ef4444" }, 6: { text: "收益", icon: "💰", color: "#10b981" },
7: { text: "提出", icon: "🔄", color: "#6366f1" }, 8: { text: "开卡", icon: "🚫", color: "#6b7280" },
14: { text: "红包", icon: "🧧", color: "#f43f5e" }
};
return types[type] || { text: `类型 ${type}`, icon: "❓", color: "#6b7280" };
}
function formatMetadata(metadata) {
if (typeof metadata !== 'object' || metadata === null || Object.keys(metadata).length === 0) {
return '<p class="detail-item"><strong>元数据:</strong> <span>无</span></p>';
}
let html = '<div class="metadata-title">元数据:</div>';
for (const key in metadata) {
html += `<div class="metadata-item"><strong>${key}:</strong> <span>${typeof metadata[key] === 'object' ? JSON.stringify(metadata[key]) : metadata[key]}</span></div>`;
}
return html;
}
function renderStatementRecords(apiResponse) {
recordsContainer.innerHTML = '';
loadingIndicator.style.display = 'none';
if (!apiResponse || !apiResponse.data || !apiResponse.data.items) {
recordsContainer.innerHTML = '<p>无法加载账单数据或无记录。</p>';
updatePaginationControls(0);
return;
}
const items = apiResponse.data.items;
totalRecords = apiResponse.data.total || items.length;
if (items.length === 0) {
recordsContainer.innerHTML = '<p>该账户暂无账单记录。</p>';
updatePaginationControls(0);
return;
}
items.forEach(item => {
const statementItemCard = document.createElement('div');
statementItemCard.classList.add('statement-card-item');
const changeTypeDetails = getChangeTypeDetails(item.change_type);
const amountClass = item.change > 0 ? 'positive' : (item.change < 0 ? 'negative' : 'neutral');
const formattedAmount = `${item.change >= 0 ? '+' : ''}${parseFloat(item.change).toFixed(item.field === "RED_PACKET_BALANCE" || item.field === "AVAILABLE_BALANCE" ? 5 : 2)}`;
statementItemCard.innerHTML = `
<div class="card-summary">
<div class="summary-left">
<div class="change-type-icon" style="background-color: ${changeTypeDetails.color};">${changeTypeDetails.icon}</div>
<div class="summary-info">
<div class="description">${changeTypeDetails.text} - ${item.field === "AVAILABLE_BALANCE" ? "可用余额" : (item.field === "RED_PACKET_BALANCE" ? "红包余额" : item.field)}</div>
<div class="timestamp">${formatStatementTimestamp(item.created_at)}</div>
</div>
</div>
<div class="summary-right"><span class="amount ${amountClass}">${formattedAmount}</span></div>
</div>
<div class="card-details">
<div class="details-grid">
<p class="detail-item"><strong>ID:</strong> <span>${item.id}</span></p>
<p class="detail-item"><strong>交易ID:</strong> <span>${item.tx_id}</span></p>
<p class="detail-item"><strong>字段:</strong> <span>${item.field}</span></p>
<p class="detail-item"><strong>变动类型:</strong> <span>${changeTypeDetails.text} (${item.change_type})</span></p>
<p class="detail-item"><strong>变动额:</strong> <span class="${amountClass}">${formattedAmount}</span></p>
<p class="detail-item"><strong>状态:</strong> <span>${item.status === 1 ? '成功' : '处理中/失败'}</span></p>
<p class="detail-item"><strong>变动前余额:</strong> <span>${parseFloat(item.pre_balance).toFixed(5)}</span></p>
<p class="detail-item"><strong>变动后余额:</strong> <span>${parseFloat(item.balance).toFixed(5)}</span></p>
</div>
${formatMetadata(item.metadata)}
</div>
`;
statementItemCard.querySelector('.card-summary').addEventListener('click', () => {
const details = statementItemCard.querySelector('.card-details');
details.style.display = details.style.display === 'none' || details.style.display === '' ? 'block' : 'none';
});
recordsContainer.appendChild(statementItemCard);
});
updatePaginationControls(totalRecords);
}
function updatePaginationControls(total) {
const totalPages = Math.ceil(total / pageSize);
pageInfoElem.textContent = `第 ${currentPage} / ${totalPages} 页 (共 ${total} 条)`;
prevPageBtn.disabled = currentPage === 1;
nextPageBtn.disabled = currentPage === totalPages || totalPages === 0;
}
async function fetchStatementsForPage(page) {
loadingIndicator.style.display = 'block';
recordsContainer.innerHTML = '';
try {
const response = await fetch(`/api/account_statements?email=${encodeURIComponent(accountEmail)}&page=${page}&size=${pageSize}`);
if (response.status === 401) { window.location.href = '/'; return; }
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
const statementData = await response.json();
if (statementData.error) {
recordsContainer.innerHTML = `<p style="color: var(--error-color);">获取账单失败: ${statementData.error}</p>`;
} else {
renderStatementRecords(statementData);
}
} catch (error) {
console.error(`Error fetching statements for ${accountEmail} page ${page}:`, error);
recordsContainer.innerHTML = `<p style="color: var(--error-color);">获取账单时发生网络错误。</p>`;
}
}
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
fetchStatementsForPage(currentPage);
}
});
nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(totalRecords / pageSize);
if (currentPage < totalPages) {
currentPage++;
fetchStatementsForPage(currentPage);
}
});
document.addEventListener('DOMContentLoaded', function() {
if (accountEmail) {
fetchAccountStatistics();
fetchStatementsForPage(currentPage);
} else {
recordsContainer.innerHTML = "<p>未指定账户信息。</p>";
loadingIndicator.style.display = 'none';
summaryTotalIncomeElem.textContent = 'N/A';
summaryTotalExpenseElem.textContent = 'N/A';
summaryTotalBalanceElem.textContent = 'N/A';
}
});
async function fetchAccountStatistics() {
if (!accountEmail) return;
try {
const response = await fetch(`/api/account_summary_statistics?email=${encodeURIComponent(accountEmail)}`);
if (response.status === 401) {
console.warn('Unauthorized access to statistics API via backend. User might need to log in.');
summaryTotalIncomeElem.textContent = '认证失败';
summaryTotalExpenseElem.textContent = '认证失败';
summaryTotalBalanceElem.textContent = '认证失败';
return;
}
if (!response.ok) {
let errorMsg = `HTTP error! status: ${response.status}`;
try {
const errData = await response.json();
errorMsg = errData.error || errData.message || errorMsg;
} catch (e) { }
throw new Error(errorMsg);
}
const statsData = await response.json();
if (statsData.error) {
console.error('Backend failed to get statistics:', statsData.error);
summaryTotalIncomeElem.textContent = '加载失败';
summaryTotalExpenseElem.textContent = '加载失败';
summaryTotalBalanceElem.textContent = '加载失败';
} else if (statsData.code === 0 && statsData.data) {
summaryTotalIncomeElem.textContent = `${parseFloat(statsData.data.income || 0).toFixed(2)} USD`;
summaryTotalExpenseElem.textContent = `${parseFloat(statsData.data.expense || 0).toFixed(2)} USD`;
summaryTotalBalanceElem.textContent = `${parseFloat(statsData.data.balance || 0).toFixed(2)} USD`;
} else {
console.error('Received unexpected statistics data structure from backend:', statsData);
summaryTotalIncomeElem.textContent = '数据格式错误';
summaryTotalExpenseElem.textContent = '数据格式错误';
summaryTotalBalanceElem.textContent = '数据格式错误';
}
} catch (error) {
console.error('Error fetching account statistics via backend:', error);
summaryTotalIncomeElem.textContent = '网络错误';
summaryTotalExpenseElem.textContent = '网络错误';
summaryTotalBalanceElem.textContent = '网络错误';
}
}
</script>
</body>
</html>