782 lines
28 KiB
HTML
782 lines
28 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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
/* ── 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; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ── Navbar ── -->
|
||
<nav class="navbar navbar-nac navbar-expand-lg">
|
||
<div class="container">
|
||
<a class="navbar-brand navbar-brand-nac" href="/">
|
||
<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>
|
||
|
||
<!-- ── Hero Search ── -->
|
||
<section class="hero-section">
|
||
<div class="container">
|
||
<div class="row justify-content-center">
|
||
<div class="col-lg-8">
|
||
<h1 class="hero-title">NAC NewAssetChain 区块浏览器</h1>
|
||
<p class="hero-subtitle">搜索区块、交易哈希、地址或 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">
|
||
<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>
|
||
|
||
<!-- ── Main Content ── -->
|
||
<section class="main-section">
|
||
<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="loadMoreBlocks(); 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>
|
||
|
||
<!-- ── 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 blockPage = 1;
|
||
let txPage = 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 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 === '0x0000000000000000000000000000000000000000' || !tx.to || tx.to === '0000000000000000000000000000000000000000000000000000000000000000') {
|
||
return { label: '系统', cls: 'tx-type-heartbeat' };
|
||
}
|
||
return { label: '交易', cls: 'tx-type-other' };
|
||
}
|
||
|
||
// ── 加载网络统计 ──
|
||
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');
|
||
|
||
// Chain Info Grid
|
||
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">
|
||
<div class="block-num-badge">#${formatNumber(b.number || b.height)}</div>
|
||
<div style="flex:1;min-width:0">
|
||
<div class="block-hash" title="${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?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 (const tx of b.transactions) {
|
||
allTxs.push({ ...tx, blockNumber: b.number || b.height, blockTimestamp: b.timestamp });
|
||
}
|
||
}
|
||
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 => {
|
||
const type = getTxType(tx);
|
||
const hash = tx.hash || tx.txHash || '';
|
||
const from = tx.from || '';
|
||
return `
|
||
<div class="tx-item">
|
||
<div class="d-flex align-items-center gap-2 mb-1">
|
||
<a class="tx-hash" href="#" title="${hash}" onclick="searchByHash('${hash}'); return false;">${shortHash(hash, 14) || '(系统交易)'}</a>
|
||
<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>
|
||
${tx.blockNumber ? `<span class="ms-2">区块 #${formatNumber(tx.blockNumber)}</span>` : ''}
|
||
${tx.blockTimestamp ? `<span class="ms-2">${timeAgo(tx.blockTimestamp)}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// ── 搜索功能 ──
|
||
async function doSearch() {
|
||
const query = document.getElementById('searchInput').value.trim();
|
||
if (!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 = renderBlockDetail(b);
|
||
return;
|
||
}
|
||
}
|
||
// 尝试作为区块哈希搜索
|
||
const bRes = await fetch(`${API_BASE}/blocks/${query}`);
|
||
if (bRes.ok) {
|
||
const json = await bRes.json();
|
||
contentDiv.innerHTML = renderBlockDetail(json.data || json);
|
||
return;
|
||
}
|
||
// 尝试作为交易哈希搜索
|
||
const tRes = await fetch(`${API_BASE}/transactions/${query}`);
|
||
if (tRes.ok) {
|
||
const json = await tRes.json();
|
||
contentDiv.innerHTML = renderTxDetail(json.data || json);
|
||
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">${query}</strong>" 的相关结果
|
||
</div>`;
|
||
} catch (e) {
|
||
contentDiv.innerHTML = `<div class="text-center py-3 text-danger"><i class="bi bi-exclamation-triangle me-1"></i>搜索出错:${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderBlockDetail(b) {
|
||
if (!b || (!b.number && !b.height)) return '<div class="text-muted text-center py-3">无区块数据</div>';
|
||
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(b.number || b.height)}</div></div>
|
||
<div class="col-md-6"><div class="result-title">时间戳</div><div class="result-value">${b.timestamp ? new Date(b.timestamp * 1000).toLocaleString('zh-CN') : '--'}</div></div>
|
||
<div class="col-12"><div class="result-title">区块哈希</div><div class="result-value" style="font-size:0.8rem">${b.hash || '--'}</div></div>
|
||
<div class="col-12"><div class="result-title">父区块哈希</div><div class="result-value" style="font-size:0.8rem">${b.parentHash || '--'}</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>
|
||
`;
|
||
}
|
||
|
||
function renderTxDetail(tx) {
|
||
if (!tx) return '<div class="text-muted text-center py-3">无交易数据</div>';
|
||
const type = getTxType(tx);
|
||
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">${tx.hash || tx.txHash || '--'}</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(tx.blockNumber)}</div></div>
|
||
<div class="col-12"><div class="result-title">发送方</div><div class="result-value" style="font-size:0.8rem">${tx.from || '--'}</div></div>
|
||
<div class="col-12"><div class="result-title">接收方</div><div class="result-value" style="font-size:0.8rem">${tx.to || '--'}</div></div>
|
||
<div class="col-md-4"><div class="result-title">金额</div><div class="result-value">${tx.value || '0'} NAC</div></div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ── 加载更多 ──
|
||
function loadMoreBlocks() { loadBlocks(30); }
|
||
function loadMoreTxs() { loadTxs(30); }
|
||
|
||
// ── 搜索框回车 ──
|
||
document.getElementById('searchInput').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') doSearch();
|
||
});
|
||
|
||
// ── 初始化 ──
|
||
async function init() {
|
||
await Promise.all([loadStats(), loadBlocks(), loadTxs()]);
|
||
// 每 30 秒自动刷新统计
|
||
setInterval(loadStats, 30000);
|
||
// 每 60 秒刷新区块和交易
|
||
setInterval(() => { loadBlocks(); loadTxs(); }, 60000);
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|