feat(quantum-browser): 量子全息探索者 v2 - 区块/交易/地址详情页

新增功能:
- URL hash 路由 (#block/N, #tx/HASH, #address/ADDR)
- 区块详情页:完整字段 + 交易列表 + 前后导航
- 交易详情页:支持 hash 和 block+idx 查询
- 地址详情页:余额 + 交易历史
- 区块列表页:分页浏览
- 所有列表中的可点击链接

备份: index_v1_backup.html (原始版本)
生产: /www/wwwroot/explorer.newassetchain.io/index.html
工单: TICKET-EXPLORER-002 (2026-03-01)
This commit is contained in:
nacadmin 2026-03-01 15:19:52 +08:00
parent 4afa3da6c6
commit 59a19d3e61
3 changed files with 2448 additions and 0 deletions

View File

@ -0,0 +1,29 @@
# NAC 量子全息探索者 (Quantum Holographic Explorer)
## 版本历史
| 版本 | 文件 | 日期 | 说明 |
|------|------|------|------|
| v1 | index_v1_backup.html | 2026-02-21 | 初始版本:基础区块列表、搜索 |
| v2 | index_v2.html | 2026-03-01 | 增强版:区块/交易/地址详情页、URL hash 路由 |
## v2 新增功能
- **URL hash 路由**`#block/N`、`#tx/HASH`、`#address/ADDR`
- **区块详情页**:完整字段展示 + 交易列表 + 前后区块导航
- **交易详情页**:支持 hash 和 block+idx 两种查询方式
- **地址详情页**:余额 + 交易历史
- **区块列表页**:分页浏览
- **可点击链接**:区块/交易/地址均可点击跳转
## 部署路径
- 生产环境:`/www/wwwroot/explorer.newassetchain.io/index.html`
- 访问地址https://explorer.newassetchain.io
## API 依赖
- Explorer API`/api/v1/` (nac-lens 服务,端口 3001)
- 区块查询:`GET /api/v1/blocks/:number`
- 交易查询:`GET /api/v1/transactions/latest`
- 地址查询:`GET /api/v1/addresses/:address`

View File

@ -0,0 +1,781 @@
<!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> 量子区块浏览器 &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 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>

File diff suppressed because it is too large Load Diff