NAC_Blockchain/nac-quantum-browser/index_v2.html

1639 lines
66 KiB
HTML
Raw 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>NAC 量子区块浏览器 | NewAssetChain Explorer</title>
<meta name="description" content="NAC NewAssetChain 量子区块浏览器 - 实时查看 NAC 公链区块、交易、地址和智能合约数据">
<!-- Bootstrap 5 CDN国内可访问 -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css">
<style>
:root {
--nac-primary: #0d6efd;
--nac-dark: #0a0f1e;
--nac-card: #111827;
--nac-border: #1e2d4a;
--nac-accent: #00d4ff;
--nac-green: #00ff88;
--nac-text: #e2e8f0;
--nac-muted: #94a3b8;
}
body {
background: var(--nac-dark);
color: var(--nac-text);
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
min-height: 100vh;
}
/* ── Navbar ── */
.navbar-nac {
background: linear-gradient(90deg, #0a0f1e 0%, #0d1b2e 100%);
border-bottom: 1px solid var(--nac-border);
padding: 0.75rem 0;
}
.navbar-brand-nac {
font-size: 1.4rem;
font-weight: 700;
color: var(--nac-accent) !important;
letter-spacing: 0.05em;
}
.navbar-brand-nac span { color: #fff; }
/* ── Hero Search ── */
.hero-section {
background: linear-gradient(135deg, #0a0f1e 0%, #0d1b2e 50%, #091428 100%);
border-bottom: 1px solid var(--nac-border);
padding: 2.5rem 0 2rem;
}
.hero-title {
font-size: 1.8rem;
font-weight: 700;
color: #fff;
margin-bottom: 0.5rem;
}
.hero-subtitle {
color: var(--nac-muted);
font-size: 0.95rem;
margin-bottom: 1.5rem;
}
.search-box {
background: #111827;
border: 1px solid var(--nac-border);
border-radius: 12px;
overflow: hidden;
display: flex;
align-items: center;
}
.search-box input {
background: transparent;
border: none;
color: var(--nac-text);
padding: 0.85rem 1.2rem;
font-size: 0.95rem;
flex: 1;
outline: none;
}
.search-box input::placeholder { color: var(--nac-muted); }
.search-box button {
background: var(--nac-primary);
border: none;
color: #fff;
padding: 0.85rem 1.5rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.search-box button:hover { background: #0b5ed7; }
/* ── Stats Cards ── */
.stats-section {
padding: 1.5rem 0;
background: #0d1420;
border-bottom: 1px solid var(--nac-border);
}
.stat-card {
background: var(--nac-card);
border: 1px solid var(--nac-border);
border-radius: 12px;
padding: 1.2rem 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
height: 100%;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
flex-shrink: 0;
}
.stat-icon.blue { background: rgba(13,110,253,0.15); color: #60a5fa; }
.stat-icon.green { background: rgba(0,255,136,0.1); color: var(--nac-green); }
.stat-icon.cyan { background: rgba(0,212,255,0.1); color: var(--nac-accent); }
.stat-icon.purple { background: rgba(139,92,246,0.15); color: #a78bfa; }
.stat-icon.orange { background: rgba(251,146,60,0.15); color: #fb923c; }
.stat-label {
font-size: 0.78rem;
color: var(--nac-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.2rem;
}
.stat-value {
font-size: 1.4rem;
font-weight: 700;
color: #fff;
line-height: 1;
}
.stat-sub {
font-size: 0.75rem;
color: var(--nac-muted);
margin-top: 0.2rem;
}
/* ── Main Content ── */
.main-section { padding: 2rem 0; }
.section-card {
background: var(--nac-card);
border: 1px solid var(--nac-border);
border-radius: 12px;
overflow: hidden;
}
.section-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--nac-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title {
font-size: 1rem;
font-weight: 600;
color: #fff;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title i { color: var(--nac-accent); }
.view-all-link {
font-size: 0.82rem;
color: var(--nac-primary);
text-decoration: none;
cursor: pointer;
}
.view-all-link:hover { color: var(--nac-accent); }
/* ── Block List ── */
.block-item {
padding: 0.9rem 1.5rem;
border-bottom: 1px solid rgba(30,45,74,0.5);
display: flex;
align-items: center;
gap: 1rem;
transition: background 0.15s;
cursor: pointer;
}
.block-item:last-child { border-bottom: none; }
.block-item:hover { background: rgba(255,255,255,0.02); }
.block-num-badge {
background: rgba(13,110,253,0.15);
color: #60a5fa;
border-radius: 8px;
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
font-weight: 600;
min-width: 70px;
text-align: center;
flex-shrink: 0;
}
.block-hash {
font-size: 0.82rem;
color: var(--nac-muted);
font-family: 'Courier New', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
.block-tx-count {
font-size: 0.8rem;
color: var(--nac-green);
flex-shrink: 0;
}
.block-time {
font-size: 0.78rem;
color: var(--nac-muted);
flex-shrink: 0;
margin-left: auto;
}
/* ── TX List ── */
.tx-item {
padding: 0.9rem 1.5rem;
border-bottom: 1px solid rgba(30,45,74,0.5);
transition: background 0.15s;
cursor: pointer;
}
.tx-item:last-child { border-bottom: none; }
.tx-item:hover { background: rgba(255,255,255,0.02); }
.tx-hash {
font-size: 0.82rem;
color: #60a5fa;
font-family: 'Courier New', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 280px;
text-decoration: none;
}
.tx-hash:hover { color: var(--nac-accent); }
.tx-type-badge {
font-size: 0.72rem;
padding: 0.2rem 0.6rem;
border-radius: 20px;
font-weight: 600;
}
.tx-type-transfer { background: rgba(0,255,136,0.1); color: var(--nac-green); }
.tx-type-contract { background: rgba(139,92,246,0.15); color: #a78bfa; }
.tx-type-node { background: rgba(251,146,60,0.15); color: #fb923c; }
.tx-type-heartbeat { background: rgba(148,163,184,0.1); color: var(--nac-muted); }
.tx-type-other { background: rgba(13,110,253,0.1); color: #60a5fa; }
/* ── Network Info ── */
.network-badge {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: rgba(0,255,136,0.1);
color: var(--nac-green);
border: 1px solid rgba(0,255,136,0.2);
border-radius: 20px;
padding: 0.25rem 0.8rem;
font-size: 0.8rem;
font-weight: 600;
}
.network-badge::before {
content: '';
width: 7px;
height: 7px;
background: var(--nac-green);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* ── Chain Info Panel ── */
.chain-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.chain-info-item {
padding: 0.9rem 1.5rem;
border-bottom: 1px solid rgba(30,45,74,0.5);
border-right: 1px solid rgba(30,45,74,0.5);
}
.chain-info-item:nth-child(2n) { border-right: none; }
.chain-info-item:nth-last-child(-n+2) { border-bottom: none; }
.chain-info-label {
font-size: 0.75rem;
color: var(--nac-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.3rem;
}
.chain-info-value {
font-size: 0.9rem;
font-weight: 600;
color: #fff;
font-family: 'Courier New', monospace;
}
.chain-info-value.accent { color: var(--nac-accent); }
.chain-info-value.green { color: var(--nac-green); }
/* ── Loading ── */
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: var(--nac-muted);
gap: 0.5rem;
}
/* ── Search Result ── */
#searchResult {
display: none;
margin-top: 1rem;
}
.result-card {
background: var(--nac-card);
border: 1px solid var(--nac-border);
border-radius: 12px;
padding: 1.5rem;
}
.result-title {
font-size: 0.8rem;
color: var(--nac-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.result-value {
font-size: 0.9rem;
color: var(--nac-text);
font-family: 'Courier New', monospace;
word-break: break-all;
}
/* ── Detail Page ── */
.detail-page { display: none; padding: 2rem 0; }
.detail-page.active { display: block; }
.breadcrumb-nac {
font-size: 0.82rem;
color: var(--nac-muted);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.breadcrumb-nac a { color: var(--nac-accent); text-decoration: none; }
.breadcrumb-nac a:hover { text-decoration: underline; }
.detail-table td:first-child {
color: var(--nac-muted);
font-size: 0.85rem;
width: 180px;
vertical-align: top;
padding-top: 0.85rem;
}
.detail-table td:last-child {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
word-break: break-all;
}
.hash-link {
color: var(--nac-accent);
text-decoration: none;
cursor: pointer;
}
.hash-link:hover { text-decoration: underline; }
.nav-btn {
background: rgba(13,110,253,0.15);
border: 1px solid rgba(13,110,253,0.3);
color: #60a5fa;
border-radius: 8px;
padding: 0.4rem 0.9rem;
font-size: 0.82rem;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.nav-btn:hover { background: rgba(13,110,253,0.25); color: #60a5fa; }
.nav-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.data-pre {
background: #080d18;
border: 1px solid var(--nac-border);
border-radius: 8px;
padding: 1rem;
font-size: 0.78rem;
color: var(--nac-muted);
word-break: break-all;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
.badge-confirmed { background: rgba(0,255,136,0.15); color: var(--nac-green); border: 1px solid rgba(0,255,136,0.3); }
.badge-pending { background: rgba(251,146,60,0.15); color: #fb923c; border: 1px solid rgba(251,146,60,0.3); }
.badge-type {
font-size: 0.75rem;
padding: 0.2rem 0.7rem;
border-radius: 20px;
font-weight: 600;
border: 1px solid transparent;
}
/* ── Footer ── */
.footer-nac {
background: #080d18;
border-top: 1px solid var(--nac-border);
padding: 1.5rem 0;
color: var(--nac-muted);
font-size: 0.82rem;
text-align: center;
}
.footer-nac a { color: var(--nac-accent); text-decoration: none; }
/* ── Responsive ── */
@media (max-width: 768px) {
.hero-title { font-size: 1.4rem; }
.stat-value { font-size: 1.2rem; }
.chain-info-grid { grid-template-columns: 1fr; }
.chain-info-item { border-right: none; }
.chain-info-item:nth-last-child(-n+2) { border-bottom: 1px solid rgba(30,45,74,0.5); }
.chain-info-item:last-child { border-bottom: none; }
.detail-table td:first-child { width: 120px; }
}
</style>
</head>
<body>
<!-- ── Navbar ── -->
<nav class="navbar navbar-nac navbar-expand-lg">
<div class="container">
<a class="navbar-brand navbar-brand-nac" href="#" onclick="navigate('home'); return false;">
<i class="bi bi-hexagon-fill me-2" style="color:var(--nac-accent)"></i>NAC <span>量子浏览器</span>
</a>
<div class="d-flex align-items-center gap-3">
<span class="network-badge" id="networkBadge">Mainnet</span>
<a href="https://newassetchain.io" target="_blank" class="btn btn-sm btn-outline-secondary" style="border-color:var(--nac-border);color:var(--nac-muted)">
<i class="bi bi-globe me-1"></i>官网
</a>
</div>
</div>
</nav>
<!-- ── 搜索栏(始终显示)── -->
<section class="hero-section" id="heroSection">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="hero-title" id="heroTitle">NAC NewAssetChain 区块浏览器</h1>
<p class="hero-subtitle" id="heroSubtitle">搜索区块、交易哈希、地址或 Charter 智能合约</p>
<div class="search-box">
<i class="bi bi-search ms-3" style="color:var(--nac-muted)"></i>
<input type="text" id="searchInput" placeholder="输入区块高度、交易哈希48字节SHA3-384或钱包地址...">
<button onclick="doSearch()"><i class="bi bi-search me-1"></i>搜索</button>
</div>
<div id="searchResult">
<div class="result-card mt-3" id="searchResultContent"></div>
</div>
</div>
</div>
</div>
</section>
<!-- ── Stats ── -->
<section class="stats-section" id="statsSection">
<div class="container">
<div class="row g-3">
<div class="col-6 col-md-4 col-lg-2-4">
<div class="stat-card">
<div class="stat-icon blue"><i class="bi bi-stack"></i></div>
<div>
<div class="stat-label">区块高度</div>
<div class="stat-value" id="statBlockHeight">--</div>
<div class="stat-sub">Block Height</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2-4">
<div class="stat-card">
<div class="stat-icon green"><i class="bi bi-arrow-left-right"></i></div>
<div>
<div class="stat-label">总交易数</div>
<div class="stat-value" id="statTotalTx">--</div>
<div class="stat-sub">Total Transactions</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2-4">
<div class="stat-card">
<div class="stat-icon cyan"><i class="bi bi-cpu"></i></div>
<div>
<div class="stat-label">共识协议</div>
<div class="stat-value" id="statConsensus" style="font-size:1.1rem">CBPP</div>
<div class="stat-sub">Constitutional Block</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2-4">
<div class="stat-card">
<div class="stat-icon purple"><i class="bi bi-code-slash"></i></div>
<div>
<div class="stat-label">智能合约</div>
<div class="stat-value" style="font-size:1.1rem">Charter</div>
<div class="stat-sub">NAC Native Language</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2-4">
<div class="stat-card">
<div class="stat-icon orange"><i class="bi bi-diagram-3"></i></div>
<div>
<div class="stat-label">节点数</div>
<div class="stat-value" id="statPeers">--</div>
<div class="stat-sub">Network Peers</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── 首页内容 ── -->
<section class="main-section" id="homePage">
<div class="container">
<div class="row g-4">
<!-- Left: Latest Blocks -->
<div class="col-lg-6">
<div class="section-card">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-stack"></i>最新区块</h2>
<a href="#" class="view-all-link" onclick="navigate('blocks'); return false;">查看全部 →</a>
</div>
<div id="blockList">
<div class="loading-spinner">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span>加载中...</span>
</div>
</div>
</div>
</div>
<!-- Right: Latest Transactions -->
<div class="col-lg-6">
<div class="section-card">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-arrow-left-right"></i>最新交易</h2>
<a href="#" class="view-all-link" onclick="loadMoreTxs(); return false;">查看更多 →</a>
</div>
<div id="txList">
<div class="loading-spinner">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span>加载中...</span>
</div>
</div>
</div>
</div>
<!-- Bottom: Chain Info -->
<div class="col-12">
<div class="section-card">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-info-circle"></i>NAC 公链信息</h2>
<span class="text-muted" style="font-size:0.8rem" id="lastUpdate">--</span>
</div>
<div class="chain-info-grid" id="chainInfoGrid">
<div class="loading-spinner" style="grid-column:1/-1">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span>加载中...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── 区块列表页 ── -->
<div class="detail-page" id="blocksPage">
<div class="container">
<div class="breadcrumb-nac">
<a href="#" onclick="navigate('home'); return false;"><i class="bi bi-house me-1"></i>首页</a>
<span>/</span>
<span>区块列表</span>
</div>
<div class="section-card">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-stack"></i>区块列表</h2>
<span class="text-muted" style="font-size:0.8rem" id="blocksPageInfo">--</span>
</div>
<div id="blocksPageList">
<div class="loading-spinner">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span>加载中...</span>
</div>
</div>
<div class="p-3 d-flex justify-content-center gap-2" id="blocksPagination"></div>
</div>
</div>
</div>
<!-- ── 区块详情页 ── -->
<div class="detail-page" id="blockDetailPage">
<div class="container">
<div class="breadcrumb-nac">
<a href="#" onclick="navigate('home'); return false;"><i class="bi bi-house me-1"></i>首页</a>
<span>/</span>
<a href="#" onclick="navigate('blocks'); return false;">区块列表</a>
<span>/</span>
<span id="blockDetailBreadcrumb">区块详情</span>
</div>
<div id="blockDetailContent">
<div class="loading-spinner">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span>加载中...</span>
</div>
</div>
</div>
</div>
<!-- ── 交易详情页 ── -->
<div class="detail-page" id="txDetailPage">
<div class="container">
<div class="breadcrumb-nac">
<a href="#" onclick="navigate('home'); return false;"><i class="bi bi-house me-1"></i>首页</a>
<span>/</span>
<span>交易详情</span>
</div>
<div id="txDetailContent">
<div class="loading-spinner">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span>加载中...</span>
</div>
</div>
</div>
</div>
<!-- ── 地址详情页 ── -->
<div class="detail-page" id="addressDetailPage">
<div class="container">
<div class="breadcrumb-nac">
<a href="#" onclick="navigate('home'); return false;"><i class="bi bi-house me-1"></i>首页</a>
<span>/</span>
<span>地址详情</span>
</div>
<div id="addressDetailContent">
<div class="loading-spinner">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span>加载中...</span>
</div>
</div>
</div>
</div>
<!-- ── Footer ── -->
<footer class="footer-nac">
<div class="container">
<p class="mb-1">
<strong style="color:var(--nac-accent)">NAC NewAssetChain</strong> 量子区块浏览器 &nbsp;|&nbsp;
共识CBPP &nbsp;|&nbsp; 虚拟机NVM 2.0 &nbsp;|&nbsp; 合约语言Charter &nbsp;|&nbsp; 资产协议ACC-20
</p>
<p class="mb-0" style="font-size:0.75rem">
<a href="https://newassetchain.io">官方网站</a> &nbsp;·&nbsp;
<a href="https://git.newassetchain.io">代码库</a> &nbsp;·&nbsp;
<a href="https://id.newassetchain.io">注册系统</a>
</p>
</div>
</footer>
<script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js"></script>
<script>
// ── API 配置 ──
const API_BASE = '/api/v1';
// ── 路由状态 ──
let currentPage = 'home';
let blocksCurrentPage = 1;
// ── 工具函数 ──
function shortHash(hash, len = 16) {
if (!hash) return '--';
if (hash.startsWith('0x')) hash = hash.slice(2);
return hash.length > len * 2 ? hash.slice(0, len) + '...' + hash.slice(-8) : hash;
}
function timeAgo(ts) {
if (!ts) return '--';
const diff = Math.floor(Date.now() / 1000) - ts;
if (diff < 60) return diff + '秒前';
if (diff < 3600) return Math.floor(diff / 60) + '分钟前';
if (diff < 86400) return Math.floor(diff / 3600) + '小时前';
return Math.floor(diff / 86400) + '天前';
}
function formatTime(ts) {
if (!ts) return '--';
return new Date(ts * 1000).toLocaleString('zh-CN');
}
function formatNumber(n) {
if (n === undefined || n === null) return '--';
return Number(n).toLocaleString('zh-CN');
}
function getTxType(tx) {
if (!tx) return { label: '未知', cls: 'tx-type-other' };
const data = tx.data || tx.input || '';
if (typeof data === 'string' && data.includes('"type"')) {
try {
const parsed = JSON.parse(data);
if (parsed.type === 'node_registration') return { label: '节点注册', cls: 'tx-type-node' };
if (parsed.type === 'heartbeat') return { label: '心跳', cls: 'tx-type-heartbeat' };
if (parsed.type === 'transfer') return { label: '转账', cls: 'tx-type-transfer' };
if (parsed.type === 'contract') return { label: '合约', cls: 'tx-type-contract' };
return { label: parsed.type || '交易', cls: 'tx-type-other' };
} catch {}
}
if (tx.value && tx.value !== '0') return { label: '转账', cls: 'tx-type-transfer' };
if (!tx.to || tx.to === '0000000000000000000000000000000000000000000000000000000000000000') {
return { label: '系统', cls: 'tx-type-heartbeat' };
}
return { label: '交易', cls: 'tx-type-other' };
}
function escHtml(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 页面路由 ──
const ALL_PAGES = ['homePage','blocksPage','blockDetailPage','txDetailPage','addressDetailPage'];
function showPage(pageId) {
ALL_PAGES.forEach(p => {
const el = document.getElementById(p);
if (el) el.classList.remove('active');
});
const target = document.getElementById(pageId);
if (target) target.classList.add('active');
// 首页显示完整 hero其他页面缩小
const heroTitle = document.getElementById('heroTitle');
const heroSubtitle = document.getElementById('heroSubtitle');
const statsSection = document.getElementById('statsSection');
const homePage = document.getElementById('homePage');
if (pageId === 'homePage') {
heroTitle.style.display = '';
heroSubtitle.style.display = '';
statsSection.style.display = '';
homePage.style.display = '';
} else {
heroTitle.style.display = 'none';
heroSubtitle.style.display = 'none';
statsSection.style.display = 'none';
homePage.style.display = 'none';
}
// 清除搜索结果
document.getElementById('searchResult').style.display = 'none';
document.getElementById('searchInput').value = '';
}
function navigate(page, param) {
currentPage = page;
if (page === 'home') {
showPage('homePage');
window.location.hash = '';
} else if (page === 'blocks') {
showPage('blocksPage');
window.location.hash = '#blocks';
loadBlocksPage(1);
} else if (page === 'block') {
showPage('blockDetailPage');
window.location.hash = '#block/' + param;
loadBlockDetail(param);
} else if (page === 'tx') {
showPage('txDetailPage');
window.location.hash = '#tx/' + param;
loadTxDetail(param);
} else if (page === 'txByBlock') {
// param = { block, idx }
showPage('txDetailPage');
window.location.hash = '#tx/block/' + param.block + '/' + param.idx;
loadTxDetailByBlock(param.block, param.idx);
} else if (page === 'address') {
showPage('addressDetailPage');
window.location.hash = '#address/' + param;
loadAddressDetail(param);
}
}
// ── Hash 路由处理 ──
function handleHashChange() {
const hash = window.location.hash.replace('#', '');
if (!hash || hash === '/') {
showPage('homePage');
return;
}
if (hash === 'blocks') {
showPage('blocksPage');
loadBlocksPage(1);
return;
}
const blockMatch = hash.match(/^block\/(\d+)$/);
if (blockMatch) {
showPage('blockDetailPage');
loadBlockDetail(parseInt(blockMatch[1]));
return;
}
const txMatch = hash.match(/^tx\/([^/]+)$/);
if (txMatch && !txMatch[1].startsWith('block')) {
showPage('txDetailPage');
loadTxDetail(txMatch[1]);
return;
}
const txBlockMatch = hash.match(/^tx\/block\/(\d+)\/(\d+)$/);
if (txBlockMatch) {
showPage('txDetailPage');
loadTxDetailByBlock(parseInt(txBlockMatch[1]), parseInt(txBlockMatch[2]));
return;
}
const addrMatch = hash.match(/^address\/(.+)$/);
if (addrMatch) {
showPage('addressDetailPage');
loadAddressDetail(addrMatch[1]);
return;
}
showPage('homePage');
}
window.addEventListener('hashchange', handleHashChange);
// ── 加载网络统计 ──
async function loadStats() {
try {
const res = await fetch(API_BASE + '/network/stats');
const json = await res.json();
const d = json.data || json;
document.getElementById('statBlockHeight').textContent = formatNumber(d.blockHeight || d.currentBlock);
document.getElementById('statTotalTx').textContent = formatNumber(d.estimatedTotalTxs || d.totalTxs || 0);
document.getElementById('statConsensus').textContent = d.consensus || 'CBPP';
document.getElementById('statPeers').textContent = formatNumber(d.peers || 0);
document.getElementById('lastUpdate').textContent = '最后更新:' + new Date().toLocaleTimeString('zh-CN');
const chainData = [
{ label: '链 ID', value: d.chainId || '5132611', cls: 'accent' },
{ label: '链 ID (Hex)', value: d.chainIdHex || '0x4E4143', cls: 'accent' },
{ label: '网络类型', value: d.network || 'mainnet', cls: 'green' },
{ label: '共识协议', value: d.consensus || 'CBPP', cls: 'green' },
{ label: '虚拟机版本', value: d.nvmVersion ? 'NVM ' + d.nvmVersion : 'NVM 2.0', cls: '' },
{ label: '智能合约语言', value: d.smartContractLanguage || 'Charter', cls: '' },
{ label: '资产协议', value: d.assetProtocol || 'ACC-20', cls: '' },
{ label: '区块生产模式', value: d.blockProductionMode || 'transaction-driven', cls: '' },
{ label: '宪法层', value: d.constitutionLayer ? '已激活' : '未激活', cls: d.constitutionLayer ? 'green' : '' },
{ label: '流体区块模式', value: d.fluidBlockMode ? '已启用' : '未启用', cls: d.fluidBlockMode ? 'green' : '' },
{ label: '网络协议', value: 'CSNP', cls: '' },
{ label: 'RPC 协议', value: 'NRPC 4.0', cls: '' },
];
document.getElementById('chainInfoGrid').innerHTML = chainData.map(item => `
<div class="chain-info-item">
<div class="chain-info-label">${item.label}</div>
<div class="chain-info-value ${item.cls}">${item.value}</div>
</div>
`).join('');
} catch (e) {
console.error('Stats load error:', e);
}
}
// ── 首页:加载区块列表 ──
async function loadBlocks(limit = 10) {
try {
const res = await fetch(`${API_BASE}/blocks?limit=${limit}`);
const json = await res.json();
const blocks = (json.data && json.data.blocks) ? json.data.blocks : (Array.isArray(json.data) ? json.data : []);
if (!blocks.length) {
document.getElementById('blockList').innerHTML = '<div class="loading-spinner text-muted">暂无区块数据</div>';
return;
}
document.getElementById('blockList').innerHTML = blocks.map(b => `
<div class="block-item" onclick="navigate('block', ${b.number || b.height})">
<div class="block-num-badge">#${formatNumber(b.number || b.height)}</div>
<div style="flex:1;min-width:0">
<div class="block-hash" title="${escHtml(b.hash)}">${shortHash(b.hash)}</div>
<div style="font-size:0.75rem;color:var(--nac-muted);margin-top:2px">
<i class="bi bi-arrow-left-right me-1"></i>${b.txCount || b.transactionCount || 0} 笔交易
</div>
</div>
<div class="block-time">${timeAgo(b.timestamp)}</div>
</div>
`).join('');
} catch (e) {
document.getElementById('blockList').innerHTML = '<div class="loading-spinner text-danger"><i class="bi bi-exclamation-triangle me-1"></i>加载失败</div>';
}
}
// ── 首页:加载交易列表 ──
async function loadTxs(limit = 10) {
try {
const res = await fetch(`${API_BASE}/transactions/latest?limit=${limit}`);
const json = await res.json();
const txs = (json.data && json.data.transactions) ? json.data.transactions :
(json.data && Array.isArray(json.data)) ? json.data : [];
if (!txs.length) {
// 从区块中提取交易
const bRes = await fetch(`${API_BASE}/blocks?limit=20`);
const bJson = await bRes.json();
const blocks = (bJson.data && bJson.data.blocks) ? bJson.data.blocks : [];
const allTxs = [];
for (const b of blocks) {
if (b.transactions && b.transactions.length > 0) {
for (let i = 0; i < b.transactions.length; i++) {
allTxs.push({ ...b.transactions[i], blockNumber: b.number || b.height, blockTimestamp: b.timestamp, _blockIdx: i });
}
}
if (allTxs.length >= limit) break;
}
if (!allTxs.length) {
document.getElementById('txList').innerHTML = '<div class="loading-spinner text-muted">暂无交易数据</div>';
return;
}
renderTxList(allTxs);
return;
}
renderTxList(txs);
} catch (e) {
document.getElementById('txList').innerHTML = '<div class="loading-spinner text-danger"><i class="bi bi-exclamation-triangle me-1"></i>加载失败</div>';
}
}
function renderTxList(txs) {
document.getElementById('txList').innerHTML = txs.map((tx, i) => {
const type = getTxType(tx);
const hash = tx.hash || tx.txHash || '';
const from = tx.from || '';
const blockNum = tx.blockNumber || tx.blockHeight || 0;
const txIdx = tx._blockIdx !== undefined ? tx._blockIdx : 0;
const clickHandler = hash
? `navigate('tx', '${escHtml(hash)}')`
: `navigate('txByBlock', {block: ${blockNum}, idx: ${txIdx}})`;
return `
<div class="tx-item" onclick="${clickHandler}">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="tx-hash" title="${escHtml(hash)}">${shortHash(hash, 14) || '(系统交易)'}</span>
<span class="tx-type-badge ${type.cls}">${type.label}</span>
</div>
<div style="font-size:0.75rem;color:var(--nac-muted)">
<span>发送方:${shortHash(from, 10) || '--'}</span>
${blockNum ? `<span class="ms-2">区块 <a class="hash-link" onclick="event.stopPropagation(); navigate('block', ${blockNum})">#${formatNumber(blockNum)}</a></span>` : ''}
${tx.blockTimestamp ? `<span class="ms-2">${timeAgo(tx.blockTimestamp)}</span>` : ''}
</div>
</div>
`;
}).join('');
}
// ── 区块列表页 ──
async function loadBlocksPage(page = 1) {
blocksCurrentPage = page;
const limit = 25;
document.getElementById('blocksPageList').innerHTML = '<div class="loading-spinner"><div class="spinner-border spinner-border-sm text-primary" role="status"></div><span>加载中...</span></div>';
try {
const res = await fetch(`${API_BASE}/blocks?limit=${limit}&page=${page}`);
const json = await res.json();
const blocks = (json.data && json.data.blocks) ? json.data.blocks : [];
const total = json.data && json.data.total ? json.data.total : blocks.length;
const totalPages = Math.max(1, Math.ceil(total / limit));
document.getElementById('blocksPageInfo').textContent = `${formatNumber(total)} 个区块`;
if (!blocks.length) {
document.getElementById('blocksPageList').innerHTML = '<div class="loading-spinner text-muted">暂无区块数据</div>';
return;
}
document.getElementById('blocksPageList').innerHTML = `
<div class="table-responsive">
<table class="table table-dark table-hover mb-0" style="font-size:0.85rem">
<thead style="background:#0d1420">
<tr>
<th style="color:var(--nac-muted);font-weight:500;padding:0.85rem 1.5rem">区块高度</th>
<th style="color:var(--nac-muted);font-weight:500">哈希</th>
<th style="color:var(--nac-muted);font-weight:500">交易数</th>
<th style="color:var(--nac-muted);font-weight:500">验证者</th>
<th style="color:var(--nac-muted);font-weight:500">时间</th>
</tr>
</thead>
<tbody>
${blocks.map(b => `
<tr onclick="navigate('block', ${b.number || b.height})" style="cursor:pointer">
<td style="padding:0.85rem 1.5rem"><span style="color:#60a5fa;font-weight:600">#${formatNumber(b.number || b.height)}</span></td>
<td><span style="font-family:monospace;color:var(--nac-muted)">${shortHash(b.hash, 12)}</span></td>
<td><span style="color:var(--nac-green)">${b.txCount || b.transactionCount || 0}</span></td>
<td><span style="font-family:monospace;color:var(--nac-muted)">${shortHash(b.validator || b.producer || '', 8)}</span></td>
<td style="color:var(--nac-muted)">${timeAgo(b.timestamp)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
// 分页
let paginationHtml = '';
if (totalPages > 1) {
paginationHtml = `
<button class="nav-btn" onclick="loadBlocksPage(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
<i class="bi bi-chevron-left"></i> 上一页
</button>
<span style="color:var(--nac-muted);font-size:0.82rem">第 ${page} / ${totalPages} 页</span>
<button class="nav-btn" onclick="loadBlocksPage(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>
下一页 <i class="bi bi-chevron-right"></i>
</button>
`;
}
document.getElementById('blocksPagination').innerHTML = paginationHtml;
} catch (e) {
document.getElementById('blocksPageList').innerHTML = '<div class="loading-spinner text-danger"><i class="bi bi-exclamation-triangle me-1"></i>加载失败</div>';
}
}
// ── 区块详情页 ──
async function loadBlockDetail(blockNumber) {
document.getElementById('blockDetailBreadcrumb').textContent = `区块 #${blockNumber}`;
document.getElementById('blockDetailContent').innerHTML = '<div class="loading-spinner"><div class="spinner-border spinner-border-sm text-primary" role="status"></div><span>加载中...</span></div>';
try {
const res = await fetch(`${API_BASE}/blocks/${blockNumber}`);
const json = await res.json();
const b = json.data || json;
if (!b || (!b.number && !b.height)) {
document.getElementById('blockDetailContent').innerHTML = renderNotFound('区块', blockNumber);
return;
}
const num = b.number || b.height;
const isHeartbeat = b.isHeartbeat || false;
const txs = b.transactions || [];
document.getElementById('blockDetailContent').innerHTML = `
<!-- 导航按钮 -->
<div class="d-flex align-items-center gap-2 mb-3">
<button class="nav-btn" onclick="navigate('block', ${num - 1})" ${num <= 0 ? 'disabled' : ''}>
<i class="bi bi-chevron-left"></i> 上一区块
</button>
<span style="color:#fff;font-weight:600;font-size:1.1rem">区块 #${formatNumber(num)}</span>
<button class="nav-btn" onclick="navigate('block', ${num + 1})">
下一区块 <i class="bi bi-chevron-right"></i>
</button>
${isHeartbeat ? '<span class="badge-type tx-type-heartbeat ms-2">心跳块</span>' : '<span class="badge-type tx-type-other ms-2">交易块</span>'}
</div>
<!-- 区块基本信息 -->
<div class="section-card mb-3">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-cube"></i>区块信息</h2>
<span class="badge-type badge-confirmed">已确认</span>
</div>
<div class="p-0">
<table class="table table-dark table-borderless detail-table mb-0">
<tbody>
<tr><td>区块高度</td><td><strong style="color:#60a5fa">#${formatNumber(num)}</strong></td></tr>
<tr><td>区块哈希</td><td><span style="color:var(--nac-accent)">${escHtml(b.hash || 'N/A')}</span></td></tr>
<tr><td>父区块哈希</td><td>
${b.parentHash && num > 0
? `<a class="hash-link" onclick="navigate('block', ${num - 1})">${escHtml(b.parentHash)}</a>`
: escHtml(b.parentHash || 'N/A')}
</td></tr>
<tr><td>时间戳</td><td>${formatTime(b.timestamp)} <span style="color:var(--nac-muted);font-size:0.8rem">(${timeAgo(b.timestamp)})</span></td></tr>
<tr><td>交易数</td><td><strong style="color:var(--nac-green)">${b.txCount || b.transactionCount || txs.length}</strong></td></tr>
<tr><td>验证者</td><td>
${b.validator || b.producer
? `<a class="hash-link" onclick="navigate('address', '${escHtml(b.validator || b.producer)}')">${escHtml(b.validator || b.producer)}</a>`
: 'N/A'}
</td></tr>
<tr><td>CBPP 轮次</td><td>${escHtml(String(b.cbppRound || b.cbpp_round || '—'))}</td></tr>
<tr><td>Epoch</td><td>${escHtml(String(b.epoch || '0'))}</td></tr>
<tr><td>状态根</td><td>${escHtml(b.stateRoot || b.state_root || 'N/A')}</td></tr>
<tr><td>交易根</td><td>${escHtml(b.txRoot || b.tx_root || 'N/A')}</td></tr>
<tr><td>宪法层</td><td>
<span class="badge-type ${b.constitutionLayer ? 'tx-type-transfer' : 'tx-type-heartbeat'}">
${b.constitutionLayer ? '已激活' : '未激活'}
</span>
</td></tr>
<tr><td>数据来源</td><td style="color:var(--nac-muted)">${escHtml(b.dataSource || 'CBPP-Node-9545')}</td></tr>
</tbody>
</table>
</div>
</div>
<!-- 交易列表 -->
<div class="section-card">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-arrow-left-right"></i>区块内交易</h2>
<span style="color:var(--nac-muted);font-size:0.8rem">${txs.length} 笔</span>
</div>
${txs.length === 0
? '<div class="loading-spinner text-muted">本区块无交易(心跳块)</div>'
: `<div class="table-responsive">
<table class="table table-dark table-hover mb-0" style="font-size:0.82rem">
<thead style="background:#0d1420">
<tr>
<th style="color:var(--nac-muted);font-weight:500;padding:0.85rem 1.5rem">索引</th>
<th style="color:var(--nac-muted);font-weight:500">交易哈希</th>
<th style="color:var(--nac-muted);font-weight:500">类型</th>
<th style="color:var(--nac-muted);font-weight:500">发送方</th>
<th style="color:var(--nac-muted);font-weight:500">接收方</th>
<th style="color:var(--nac-muted);font-weight:500">金额</th>
</tr>
</thead>
<tbody>
${txs.map((tx, i) => {
const type = getTxType(tx);
const hash = tx.hash || '';
const clickHandler = hash
? `navigate('tx', '${escHtml(hash)}')`
: `navigate('txByBlock', {block: ${num}, idx: ${i}})`;
return `
<tr onclick="${clickHandler}" style="cursor:pointer">
<td style="padding:0.85rem 1.5rem;color:var(--nac-muted)">${i}</td>
<td><span style="font-family:monospace;color:#60a5fa">${shortHash(hash, 12) || '(无哈希)'}</span></td>
<td><span class="tx-type-badge ${type.cls}">${type.label}</span></td>
<td>
<a class="hash-link" onclick="event.stopPropagation(); navigate('address', '${escHtml(tx.from || '')}')" style="font-family:monospace">
${shortHash(tx.from || '', 10)}
</a>
</td>
<td>
${tx.to && tx.to !== '0000000000000000000000000000000000000000000000000000000000000000'
? `<a class="hash-link" onclick="event.stopPropagation(); navigate('address', '${escHtml(tx.to)}')" style="font-family:monospace">${shortHash(tx.to, 10)}</a>`
: '<span style="color:var(--nac-muted)">系统地址</span>'}
</td>
<td style="color:${tx.value && tx.value !== '0' ? 'var(--nac-green)' : 'var(--nac-muted)'}">${tx.value || '0'} XTZH</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>`
}
</div>
`;
} catch (e) {
document.getElementById('blockDetailContent').innerHTML = `<div class="loading-spinner text-danger"><i class="bi bi-exclamation-triangle me-1"></i>加载失败:${escHtml(e.message)}</div>`;
}
}
// ── 交易详情页(通过哈希)──
async function loadTxDetail(hash) {
document.getElementById('txDetailContent').innerHTML = '<div class="loading-spinner"><div class="spinner-border spinner-border-sm text-primary" role="status"></div><span>加载中...</span></div>';
try {
const res = await fetch(`${API_BASE}/transactions/${encodeURIComponent(hash)}`);
const json = await res.json();
if (json.error || !json.data) {
document.getElementById('txDetailContent').innerHTML = renderTxNotFound(hash);
return;
}
renderTxDetailPage(json.data);
} catch (e) {
document.getElementById('txDetailContent').innerHTML = `<div class="loading-spinner text-danger"><i class="bi bi-exclamation-triangle me-1"></i>加载失败:${escHtml(e.message)}</div>`;
}
}
// ── 交易详情页(通过区块+索引)──
async function loadTxDetailByBlock(blockNumber, txIndex) {
document.getElementById('txDetailContent').innerHTML = '<div class="loading-spinner"><div class="spinner-border spinner-border-sm text-primary" role="status"></div><span>加载中...</span></div>';
try {
const res = await fetch(`${API_BASE}/blocks/${blockNumber}`);
const json = await res.json();
const b = json.data || json;
if (!b || !b.transactions || !b.transactions[txIndex]) {
document.getElementById('txDetailContent').innerHTML = renderTxNotFound(`区块 #${blockNumber}${txIndex} 笔交易`);
return;
}
const tx = {
...b.transactions[txIndex],
blockNumber: b.number || b.height,
blockHash: b.hash,
blockTimestamp: b.timestamp,
};
renderTxDetailPage(tx);
} catch (e) {
document.getElementById('txDetailContent').innerHTML = `<div class="loading-spinner text-danger"><i class="bi bi-exclamation-triangle me-1"></i>加载失败:${escHtml(e.message)}</div>`;
}
}
function renderTxDetailPage(tx) {
if (!tx) {
document.getElementById('txDetailContent').innerHTML = renderTxNotFound('');
return;
}
const type = getTxType(tx);
const blockNum = tx.blockNumber || tx.blockHeight || 0;
let dataDecoded = null;
if (tx.data && typeof tx.data === 'string') {
try { dataDecoded = JSON.parse(tx.data); } catch {}
}
document.getElementById('txDetailContent').innerHTML = `
<div class="d-flex align-items-center gap-2 mb-3">
<span style="color:#fff;font-weight:600;font-size:1.1rem">交易详情</span>
<span class="badge-type badge-confirmed">已确认</span>
<span class="tx-type-badge ${type.cls}">${type.label}</span>
</div>
<div class="section-card mb-3">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-arrow-left-right"></i>交易信息</h2>
</div>
<div class="p-0">
<table class="table table-dark table-borderless detail-table mb-0">
<tbody>
<tr><td>交易哈希</td><td>
${tx.hash
? `<span style="color:var(--nac-accent)">${escHtml(tx.hash)}</span>`
: '<span style="color:var(--nac-muted)">(NAC 链节点注册交易,哈希由 NVM 内部管理)</span>'}
</td></tr>
<tr><td>状态</td><td><span class="badge-type badge-confirmed">${escHtml(tx.status || 'confirmed')}</span></td></tr>
<tr><td>所在区块</td><td>
<a class="hash-link" onclick="navigate('block', ${blockNum})">#${formatNumber(blockNum)}</a>
${tx.blockHash ? `<span style="color:var(--nac-muted);font-size:0.8rem;margin-left:0.5rem;font-family:monospace">${shortHash(tx.blockHash, 12)}</span>` : ''}
</td></tr>
<tr><td>交易时间</td><td>${formatTime(tx.blockTimestamp || tx.timestamp)} <span style="color:var(--nac-muted);font-size:0.8rem">(${timeAgo(tx.blockTimestamp || tx.timestamp)})</span></td></tr>
<tr><td>发送方</td><td>
${tx.from
? `<a class="hash-link" onclick="navigate('address', '${escHtml(tx.from)}')">${escHtml(tx.from)}</a>`
: '<span style="color:var(--nac-muted)">—</span>'}
</td></tr>
<tr><td>接收方</td><td>
${tx.to && tx.to !== '0000000000000000000000000000000000000000000000000000000000000000'
? `<a class="hash-link" onclick="navigate('address', '${escHtml(tx.to)}')">${escHtml(tx.to)}</a>`
: '<span style="color:var(--nac-muted);font-family:monospace">系统地址(节点注册/创世)</span>'}
</td></tr>
<tr><td>转账金额</td><td><strong style="color:var(--nac-green)">${escHtml(String(tx.value || '0'))}</strong> <span style="color:#fb923c">XTZH</span></td></tr>
<tr><td>Nonce</td><td style="color:var(--nac-text)">${escHtml(String(tx.nonce || 0))}</td></tr>
<tr><td>Gas 限额</td><td style="color:var(--nac-text)">${escHtml(String(tx.gas || 0))}</td></tr>
<tr><td>Gas 单价</td><td style="color:var(--nac-text)">${escHtml(String(tx.gasPrice || '0'))}</td></tr>
${tx.signature ? `<tr><td>签名</td><td style="color:var(--nac-muted)">${escHtml(tx.signature)}</td></tr>` : ''}
<tr><td>协议</td><td><span class="badge-type tx-type-other">${escHtml(tx.protocol || 'NAC')}</span></td></tr>
</tbody>
</table>
</div>
</div>
${tx.data ? `
<div class="section-card mb-3">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-code-square"></i>交易数据</h2>
${dataDecoded ? `<span class="badge-type tx-type-node">${escHtml(dataDecoded.type || 'NAC 原生')}</span>` : ''}
</div>
<div class="p-3">
${dataDecoded ? `
<div class="mb-3">
<div style="font-size:0.78rem;color:var(--nac-muted);margin-bottom:0.5rem">解析结果NAC 原生交易数据):</div>
<table class="table table-dark table-sm table-borderless mb-0" style="font-size:0.82rem">
<tbody>
${Object.entries(dataDecoded).map(([k, v]) => `
<tr>
<td style="color:var(--nac-muted);width:160px">${escHtml(k)}</td>
<td style="font-family:monospace;color:var(--nac-accent);word-break:break-all">${escHtml(String(v))}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : ''}
<div style="font-size:0.78rem;color:var(--nac-muted);margin-bottom:0.3rem">原始数据:</div>
<div class="data-pre">${escHtml(tx.data)}</div>
</div>
</div>
` : ''}
<!-- NAC 技术说明 -->
<div class="section-card">
<div class="p-3">
<div class="row g-3 text-center">
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">虚拟机</div>
<div style="font-weight:700;color:#60a5fa">NVM 2.0</div>
</div>
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">共识协议</div>
<div style="font-weight:700;color:var(--nac-green)">CBPP</div>
</div>
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">哈希算法</div>
<div style="font-weight:700;color:var(--nac-accent)">SHA3-384</div>
</div>
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">资产协议</div>
<div style="font-weight:700;color:#fb923c">ACC-20</div>
</div>
</div>
</div>
</div>
`;
}
function renderTxNotFound(key) {
return `
<div class="section-card">
<div class="p-5 text-center">
<i class="bi bi-exclamation-triangle fs-1 text-warning d-block mb-3"></i>
<h5 style="color:var(--nac-text)">交易不存在</h5>
<p style="color:var(--nac-muted)">
${key ? `查询 <code style="color:var(--nac-accent)">${escHtml(String(key).substring(0, 40))}...</code> 未找到对应交易` : '请提供有效的交易哈希或区块号'}
</p>
<p style="color:var(--nac-muted);font-size:0.82rem">NAC 链交易哈希使用 SHA3-384 算法48字节格式与以太坊不同</p>
<button class="nav-btn mt-2" onclick="navigate('home')"><i class="bi bi-house me-1"></i>返回首页</button>
</div>
</div>
`;
}
// ── 地址详情页 ──
async function loadAddressDetail(address) {
document.getElementById('addressDetailContent').innerHTML = '<div class="loading-spinner"><div class="spinner-border spinner-border-sm text-primary" role="status"></div><span>加载中...</span></div>';
try {
const [addrRes, txRes] = await Promise.all([
fetch(`${API_BASE}/addresses/${encodeURIComponent(address)}`),
fetch(`${API_BASE}/addresses/${encodeURIComponent(address)}/transactions?limit=20`)
]);
const addrJson = await addrRes.json();
const txJson = await txRes.json();
const addr = addrJson.data || {};
const txs = (txJson.data && txJson.data.transactions) ? txJson.data.transactions : [];
const scannedBlocks = txJson.data && txJson.data.scannedBlocks ? txJson.data.scannedBlocks : 0;
document.getElementById('addressDetailContent').innerHTML = `
<div class="d-flex align-items-center gap-2 mb-3">
<span style="color:#fff;font-weight:600;font-size:1.1rem">地址详情</span>
<span class="badge-type ${addr.isContract ? 'tx-type-contract' : 'tx-type-heartbeat'}">
${addr.isContract ? 'Charter 智能合约' : '普通地址'}
</span>
</div>
<!-- 地址概览 -->
<div class="section-card mb-3">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-wallet2"></i>地址概览</h2>
</div>
<div class="p-3">
<div style="font-size:0.78rem;color:var(--nac-muted);margin-bottom:0.3rem">NAC 地址32字节 / 64位十六进制</div>
<div style="font-family:monospace;color:var(--nac-accent);word-break:break-all;margin-bottom:1.5rem">${escHtml(address)}</div>
<div class="row g-3">
<div class="col-md-4">
<div class="section-card" style="border-radius:8px">
<div class="p-3 text-center">
<div style="color:var(--nac-muted);font-size:0.75rem;margin-bottom:0.3rem">NAC 余额</div>
<div style="font-size:1.6rem;font-weight:700;color:#fb923c">${escHtml(String(addr.balance || '0.000000'))}</div>
<div style="color:var(--nac-muted);font-size:0.75rem">${escHtml(addr.currency || 'NAC')}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="section-card" style="border-radius:8px">
<div class="p-3 text-center">
<div style="color:var(--nac-muted);font-size:0.75rem;margin-bottom:0.3rem">交易次数</div>
<div style="font-size:1.6rem;font-weight:700;color:#60a5fa">${formatNumber(addr.transactionCount || 0)}</div>
<div style="color:var(--nac-muted);font-size:0.75rem">笔交易</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="section-card" style="border-radius:8px">
<div class="p-3 text-center">
<div style="color:var(--nac-muted);font-size:0.75rem;margin-bottom:0.3rem">ACC-20 代币</div>
<div style="font-size:1.6rem;font-weight:700;color:var(--nac-green)">${(addr.tokens || []).length}</div>
<div style="color:var(--nac-muted);font-size:0.75rem">种代币</div>
</div>
</div>
</div>
</div>
${addr.note ? `<div class="mt-3 p-2" style="background:#080d18;border-radius:6px;font-size:0.82rem;color:var(--nac-muted)">⚠ ${escHtml(addr.note)}</div>` : ''}
</div>
</div>
<!-- 交易历史 -->
<div class="section-card">
<div class="section-header">
<h2 class="section-title"><i class="bi bi-clock-history"></i>交易历史</h2>
<span style="color:var(--nac-muted);font-size:0.8rem">
${formatNumber(txs.length)}
${scannedBlocks > 0 ? `(已扫描最近 ${formatNumber(scannedBlocks)} 个区块)` : ''}
</span>
</div>
${txs.length === 0
? `<div class="loading-spinner text-muted">
<div>暂无交易记录</div>
</div>
<div style="text-align:center;padding:0 1.5rem 1.5rem;color:var(--nac-muted);font-size:0.82rem">NAC 地址余额查询需要 NVM 状态层支持(开发中)</div>`
: `<div class="table-responsive">
<table class="table table-dark table-hover mb-0" style="font-size:0.82rem">
<thead style="background:#0d1420">
<tr>
<th style="color:var(--nac-muted);font-weight:500;padding:0.85rem 1.5rem">交易哈希</th>
<th style="color:var(--nac-muted);font-weight:500">方向</th>
<th style="color:var(--nac-muted);font-weight:500">对手方</th>
<th style="color:var(--nac-muted);font-weight:500">金额</th>
<th style="color:var(--nac-muted);font-weight:500">区块</th>
<th style="color:var(--nac-muted);font-weight:500">时间</th>
</tr>
</thead>
<tbody>
${txs.map((tx, i) => {
const hash = tx.hash || '';
const blockNum = tx.blockNumber || 0;
const isOut = (tx.from || '').toLowerCase() === address.toLowerCase();
const counterpart = isOut ? tx.to : tx.from;
const clickHandler = hash
? `navigate('tx', '${escHtml(hash)}')`
: `navigate('txByBlock', {block: ${blockNum}, idx: 0})`;
return `
<tr onclick="${clickHandler}" style="cursor:pointer">
<td style="padding:0.85rem 1.5rem">
<span style="font-family:monospace;color:#60a5fa">${shortHash(hash, 12) || '(无哈希)'}</span>
</td>
<td>
<span class="badge-type ${isOut ? 'tx-type-node' : 'tx-type-transfer'}">${isOut ? '发出' : '收入'}</span>
</td>
<td>
<a class="hash-link" onclick="event.stopPropagation(); navigate('address', '${escHtml(counterpart || '')}')" style="font-family:monospace">
${shortHash(counterpart || '', 10) || '系统'}
</a>
</td>
<td style="color:${isOut ? '#fb923c' : 'var(--nac-green)'}">
${isOut ? '-' : '+'}${escHtml(String(tx.value || '0'))} XTZH
</td>
<td>
<a class="hash-link" onclick="event.stopPropagation(); navigate('block', ${blockNum})">#${formatNumber(blockNum)}</a>
</td>
<td style="color:var(--nac-muted)">${timeAgo(tx.timestamp || tx.blockTimestamp || 0)}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>`
}
</div>
<!-- NAC 地址格式说明 -->
<div class="section-card mt-3">
<div class="p-3">
<div class="row g-3 text-center">
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">地址长度</div>
<div style="font-weight:700;color:#60a5fa">32 字节</div>
</div>
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">哈希算法</div>
<div style="font-weight:700;color:var(--nac-accent)">SHA3-384</div>
</div>
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">地址类型</div>
<div style="font-weight:700;color:var(--nac-green)">NAC 原生</div>
</div>
<div class="col-6 col-md-3">
<div style="color:var(--nac-muted);font-size:0.75rem">资产协议</div>
<div style="font-weight:700;color:#fb923c">ACC-20</div>
</div>
</div>
</div>
</div>
`;
} catch (e) {
document.getElementById('addressDetailContent').innerHTML = `<div class="loading-spinner text-danger"><i class="bi bi-exclamation-triangle me-1"></i>加载失败:${escHtml(e.message)}</div>`;
}
}
function renderNotFound(type, key) {
return `
<div class="section-card">
<div class="p-5 text-center">
<i class="bi bi-exclamation-triangle fs-1 text-warning d-block mb-3"></i>
<h5 style="color:var(--nac-text)">${escHtml(type)}不存在</h5>
<p style="color:var(--nac-muted)">未找到 <code style="color:var(--nac-accent)">${escHtml(String(key))}</code></p>
<button class="nav-btn mt-2" onclick="navigate('home')"><i class="bi bi-house me-1"></i>返回首页</button>
</div>
</div>
`;
}
// ── 搜索功能 ──
async function doSearch() {
const query = document.getElementById('searchInput').value.trim();
if (!query) return;
// 如果是纯数字,直接跳转到区块详情
if (/^\d+$/.test(query)) {
navigate('block', parseInt(query));
return;
}
// 64位十六进制32字节地址
if (/^[0-9a-fA-F]{64}$/.test(query)) {
// 先尝试作为区块哈希
try {
const res = await fetch(`${API_BASE}/blocks/${query}`);
if (res.ok) {
const json = await res.json();
if (json.data && (json.data.number || json.data.height)) {
navigate('block', json.data.number || json.data.height);
return;
}
}
} catch {}
// 再尝试作为地址
navigate('address', query);
return;
}
// 其他情况,显示搜索结果
searchByHash(query);
}
async function searchByHash(query) {
document.getElementById('searchInput').value = query;
const resultDiv = document.getElementById('searchResult');
const contentDiv = document.getElementById('searchResultContent');
resultDiv.style.display = 'block';
contentDiv.innerHTML = '<div class="loading-spinner"><div class="spinner-border spinner-border-sm text-primary" role="status"></div><span>搜索中...</span></div>';
try {
if (/^\d+$/.test(query)) {
const res = await fetch(`${API_BASE}/blocks/${query}`);
if (res.ok) {
const json = await res.json();
const b = json.data || json;
contentDiv.innerHTML = renderBlockSearchResult(b);
return;
}
}
const bRes = await fetch(`${API_BASE}/blocks/${query}`);
if (bRes.ok) {
const json = await bRes.json();
contentDiv.innerHTML = renderBlockSearchResult(json.data || json);
return;
}
const tRes = await fetch(`${API_BASE}/transactions/${query}`);
if (tRes.ok) {
const json = await tRes.json();
if (!json.error && json.data) {
contentDiv.innerHTML = renderTxSearchResult(json.data || json);
return;
}
}
// 尝试作为地址
const aRes = await fetch(`${API_BASE}/addresses/${query}`);
if (aRes.ok) {
const json = await aRes.json();
if (json.data) {
contentDiv.innerHTML = renderAddrSearchResult(json.data, query);
return;
}
}
contentDiv.innerHTML = `<div class="text-center py-3" style="color:var(--nac-muted)">
<i class="bi bi-search fs-3 d-block mb-2"></i>
未找到 "<strong style="color:#fff">${escHtml(query)}</strong>" 的相关结果
</div>`;
} catch (e) {
contentDiv.innerHTML = `<div class="text-center py-3 text-danger"><i class="bi bi-exclamation-triangle me-1"></i>搜索出错:${escHtml(e.message)}</div>`;
}
}
function renderBlockSearchResult(b) {
if (!b || (!b.number && !b.height)) return '<div class="text-muted text-center py-3">无区块数据</div>';
const num = b.number || b.height;
return `
<div class="result-title">找到区块</div>
<div class="row g-3">
<div class="col-md-6"><div class="result-title">区块高度</div><div class="result-value" style="color:#60a5fa">#${formatNumber(num)}</div></div>
<div class="col-md-6"><div class="result-title">时间戳</div><div class="result-value">${formatTime(b.timestamp)}</div></div>
<div class="col-12"><div class="result-title">区块哈希</div><div class="result-value" style="font-size:0.8rem">${escHtml(b.hash || '--')}</div></div>
<div class="col-md-4"><div class="result-title">交易数</div><div class="result-value" style="color:var(--nac-green)">${b.txCount || b.transactionCount || 0}</div></div>
<div class="col-12 mt-2">
<button class="nav-btn" onclick="navigate('block', ${num}); document.getElementById('searchResult').style.display='none'">
<i class="bi bi-arrow-right me-1"></i>查看完整区块详情
</button>
</div>
</div>
`;
}
function renderTxSearchResult(tx) {
if (!tx) return '<div class="text-muted text-center py-3">无交易数据</div>';
const type = getTxType(tx);
const blockNum = tx.blockNumber || tx.blockHeight || 0;
return `
<div class="result-title">找到交易</div>
<div class="row g-3">
<div class="col-12"><div class="result-title">交易哈希</div><div class="result-value" style="font-size:0.8rem">${escHtml(tx.hash || '--')}</div></div>
<div class="col-md-6"><div class="result-title">类型</div><div class="result-value"><span class="tx-type-badge ${type.cls}">${type.label}</span></div></div>
<div class="col-md-6"><div class="result-title">区块</div><div class="result-value" style="color:#60a5fa">#${formatNumber(blockNum)}</div></div>
<div class="col-12"><div class="result-title">发送方</div><div class="result-value" style="font-size:0.8rem">${escHtml(tx.from || '--')}</div></div>
<div class="col-md-4"><div class="result-title">金额</div><div class="result-value">${escHtml(String(tx.value || '0'))} XTZH</div></div>
<div class="col-12 mt-2">
<button class="nav-btn" onclick="navigate('tx', '${escHtml(tx.hash || '')}'); document.getElementById('searchResult').style.display='none'">
<i class="bi bi-arrow-right me-1"></i>查看完整交易详情
</button>
</div>
</div>
`;
}
function renderAddrSearchResult(addr, address) {
return `
<div class="result-title">找到地址</div>
<div class="row g-3">
<div class="col-12"><div class="result-title">地址</div><div class="result-value" style="font-size:0.8rem;color:var(--nac-accent)">${escHtml(address)}</div></div>
<div class="col-md-4"><div class="result-title">余额</div><div class="result-value" style="color:#fb923c">${escHtml(String(addr.balance || '0'))} ${escHtml(addr.currency || 'NAC')}</div></div>
<div class="col-md-4"><div class="result-title">交易数</div><div class="result-value" style="color:#60a5fa">${formatNumber(addr.transactionCount || 0)}</div></div>
<div class="col-12 mt-2">
<button class="nav-btn" onclick="navigate('address', '${escHtml(address)}'); document.getElementById('searchResult').style.display='none'">
<i class="bi bi-arrow-right me-1"></i>查看完整地址详情
</button>
</div>
</div>
`;
}
// ── 加载更多 ──
function loadMoreBlocks() { navigate('blocks'); }
function loadMoreTxs() { loadTxs(30); }
// ── 搜索框回车 ──
document.getElementById('searchInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doSearch();
});
// ── 初始化 ──
async function init() {
// 先处理 URL hash
handleHashChange();
// 加载统计数据(始终加载)
await Promise.all([loadStats(), loadBlocks(), loadTxs()]);
// 每 30 秒自动刷新统计
setInterval(loadStats, 30000);
// 每 60 秒刷新区块和交易
setInterval(() => { loadBlocks(); loadTxs(); }, 60000);
}
init();
</script>
</body>
</html>