1639 lines
66 KiB
HTML
1639 lines
66 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>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> 量子区块浏览器 |
|
||
共识:CBPP | 虚拟机:NVM 2.0 | 合约语言:Charter | 资产协议:ACC-20
|
||
</p>
|
||
<p class="mb-0" style="font-size:0.75rem">
|
||
<a href="https://newassetchain.io">官方网站</a> ·
|
||
<a href="https://git.newassetchain.io">代码库</a> ·
|
||
<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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ── 页面路由 ──
|
||
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>
|