263 lines
10 KiB
PHP
Executable File
263 lines
10 KiB
PHP
Executable File
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\controller;
|
||
|
||
use app\service\ExplorerApi;
|
||
use think\facade\View;
|
||
|
||
class Index
|
||
{
|
||
/**
|
||
* 首页:网络统计 + 最新区块列表
|
||
*/
|
||
public function index()
|
||
{
|
||
$rawStats = ExplorerApi::getStats();
|
||
$rawBlocks = ExplorerApi::getBlocks(20, 1);
|
||
|
||
// 预处理统计数据(模板只接收字符串/数字,不调用静态方法)
|
||
$stats = $this->processStats($rawStats);
|
||
|
||
// 预处理区块列表
|
||
$blocks = array_map([$this, 'processBlock'], $rawBlocks['blocks'] ?? []);
|
||
|
||
return View::fetch('index/index', [
|
||
'stats' => $stats,
|
||
'blocks' => $blocks,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 区块列表页(分页)
|
||
*/
|
||
public function blocks()
|
||
{
|
||
$page = max(1, (int) request()->param('page', 1));
|
||
$limit = 25;
|
||
$data = ExplorerApi::getBlocks($limit, $page);
|
||
|
||
$blocks = array_map([$this, 'processBlock'], $data['blocks'] ?? []);
|
||
$total = (int) ($data['total'] ?? 0);
|
||
|
||
return View::fetch('index/blocks', [
|
||
'blocks' => $blocks,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'totalPages' => max(1, (int) ceil($total / $limit)),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 区块详情页
|
||
*/
|
||
public function block()
|
||
{
|
||
$number = (int) request()->param('n', 0);
|
||
|
||
if ($number <= 0) {
|
||
return View::fetch('index/error', ['message' => '无效的区块号']);
|
||
}
|
||
|
||
$raw = ExplorerApi::getBlock($number);
|
||
|
||
if (!$raw) {
|
||
return View::fetch('index/block', [
|
||
'block' => null,
|
||
'blockNum' => $number,
|
||
'transactions' => [],
|
||
'txCount' => 0,
|
||
]);
|
||
}
|
||
|
||
$block = $this->processBlock($raw);
|
||
$block['prevNumber'] = max(0, $number - 1);
|
||
$block['nextNumber'] = $number + 1;
|
||
|
||
// 预处理交易列表
|
||
$transactions = [];
|
||
foreach ($raw['transactions'] ?? [] as $tx) {
|
||
$transactions[] = [
|
||
'shortHash' => ExplorerApi::shortHash($tx['hash'] ?? '', 10, 8),
|
||
'shortFrom' => ExplorerApi::shortHash($tx['from'] ?? '', 8, 6),
|
||
'shortTo' => ExplorerApi::shortHash($tx['to'] ?? '', 8, 6),
|
||
'value' => $tx['value'] ?? 0,
|
||
'type' => $tx['type'] ?? 'transfer',
|
||
'status' => $tx['status'] ?? 'success',
|
||
];
|
||
}
|
||
|
||
return View::fetch('index/block', [
|
||
'block' => $block,
|
||
'blockNum' => $number,
|
||
'transactions' => $transactions,
|
||
'txCount' => count($transactions),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 搜索
|
||
*/
|
||
public function search()
|
||
{
|
||
$q = trim((string) request()->param('q', ''));
|
||
$type = '';
|
||
$result = null;
|
||
|
||
if ($q !== '') {
|
||
$raw = ExplorerApi::search($q);
|
||
// API 返回格式: {"type":"block","data":{...}} 或 {"data":{"type":"block","block":{...}}}
|
||
// 兼容两种格式
|
||
$type = $raw['type'] ?? ($raw['data']['type'] ?? '');
|
||
|
||
if ($type === 'block') {
|
||
// 新格式:data 直接是区块对象
|
||
$b = $raw['data'] ?? [];
|
||
// 兼容旧格式:data.block
|
||
if (empty($b['number']) && !empty($raw['data']['block'])) {
|
||
$b = $raw['data']['block'];
|
||
}
|
||
if (!empty($b['number'])) {
|
||
$result = [
|
||
'number' => $b['number'] ?? 0,
|
||
'hash' => $b['hash'] ?? 'N/A',
|
||
'timeAgo' => ExplorerApi::timeAgo($b['timestamp'] ?? 0),
|
||
];
|
||
}
|
||
} elseif ($type === 'transaction') {
|
||
$tx = $raw['data']['tx'] ?? $raw['data'] ?? [];
|
||
if (!empty($tx['hash'])) {
|
||
$result = [
|
||
'hash' => $tx['hash'] ?? 'N/A',
|
||
'blockNumber' => $tx['blockNumber'] ?? 0,
|
||
'status' => $tx['status'] ?? 'success',
|
||
];
|
||
}
|
||
} elseif ($type === 'address') {
|
||
$addr = $raw['data']['address'] ?? $raw['data'] ?? [];
|
||
if (!empty($addr['address'])) {
|
||
$result = [
|
||
'address' => $addr['address'] ?? $q,
|
||
'balance' => $addr['balance'] ?? 0,
|
||
'txCount' => $addr['txCount'] ?? 0,
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
return View::fetch('index/search', [
|
||
'q' => htmlspecialchars($q),
|
||
'type' => $type,
|
||
'result' => $result,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 节点状态页
|
||
*/
|
||
public function nodes()
|
||
{
|
||
$rawStats = ExplorerApi::getStats();
|
||
$health = ExplorerApi::health();
|
||
|
||
$stats = $this->processStats($rawStats);
|
||
$healthJson = json_encode($health, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||
|
||
return View::fetch('index/nodes', [
|
||
'stats' => $stats,
|
||
'healthJson' => $healthJson,
|
||
]);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 私有辅助方法:数据预处理
|
||
// ─────────────────────────────────────────────
|
||
|
||
/**
|
||
* 预处理统计数据,生成模板直接可用的字段
|
||
*/
|
||
private function processStats(array $raw): array
|
||
{
|
||
$cbpp = $raw['cbppConsensus'] ?? 'unknown';
|
||
$csnp = $raw['csnpNetwork'] ?? 'unknown';
|
||
$constitution = (bool) ($raw['constitutionLayer'] ?? false);
|
||
$fluid = (bool) ($raw['fluidBlockMode'] ?? false);
|
||
|
||
return [
|
||
'currentBlock' => $raw['currentBlock'] ?? 0,
|
||
'cbppConsensus' => $cbpp,
|
||
'cbppBadge' => $this->statusBadge($cbpp),
|
||
'csnpNetwork' => $csnp,
|
||
'csnpBadge' => $this->statusBadge($csnp),
|
||
'constitutionLayer' => $constitution,
|
||
'constitutionText' => $constitution ? '已激活' : '未激活',
|
||
'constitutionBadge' => $constitution ? 'bg-success' : 'bg-secondary',
|
||
'fluidBlockMode' => $fluid,
|
||
'fluidText' => $fluid ? '已启用' : '未启用',
|
||
'fluidBadge' => $fluid ? 'bg-success' : 'bg-secondary',
|
||
'chainId' => $raw['chainId'] ?? 20260131,
|
||
'network' => $raw['network'] ?? 'mainnet',
|
||
'nodeCount' => $raw['nodeCount'] ?? 0,
|
||
'totalTransactions' => $raw['totalTransactions'] ?? 0,
|
||
'tps' => $raw['tps'] ?? 0,
|
||
'avgBlockTime' => $raw['avgBlockTime'] ?? '3.0',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 预处理单个区块数据
|
||
*/
|
||
private function processBlock(array $block): array
|
||
{
|
||
$constitution = (bool) ($block['constitutionLayer'] ?? false);
|
||
$isHeartbeat = (bool) ($block['isHeartbeat'] ?? false);
|
||
$blockType = $block['blockType'] ?? ($isHeartbeat ? 'heartbeat' : 'tx');
|
||
$blockTypeLabel = $block['blockTypeLabel'] ?? ($isHeartbeat ? '心跳块' : '交易块');
|
||
$blockTypeNote = $block['blockTypeNote'] ?? ($isHeartbeat
|
||
? 'CBPP 宪法原则四:无交易时每60秒产生心跳块,证明网络存活'
|
||
: 'CBPP 交易驱动块:包含真实链上交易');
|
||
return [
|
||
'number' => $block['number'] ?? $block['height'] ?? 0,
|
||
'hash' => $block['hash'] ?? 'N/A',
|
||
'shortHash' => ExplorerApi::shortHash($block['hash'] ?? '', 16, 12),
|
||
'parentHash' => $block['parentHash'] ?? $block['parent_hash'] ?? 'N/A',
|
||
'timestamp' => $block['timestamp'] ?? 0,
|
||
'formatTime' => ExplorerApi::formatTime($block['timestamp'] ?? 0),
|
||
'timeAgo' => ExplorerApi::timeAgo($block['timestamp'] ?? 0),
|
||
'validator' => $block['validator'] ?? $block['producer'] ?? 'N/A',
|
||
'shortValidator' => ExplorerApi::shortHash($block['validator'] ?? $block['producer'] ?? '', 8, 6),
|
||
'txCount' => $block['txCount'] ?? $block['tx_count'] ?? 0,
|
||
'size' => $block['size'] ?? 0,
|
||
'cbppRound' => $block['cbppRound'] ?? $block['cbpp_round'] ?? '—',
|
||
'epoch' => $block['epoch'] ?? 0,
|
||
'stateRoot' => $block['stateRoot'] ?? $block['state_root'] ?? 'N/A',
|
||
'txRoot' => $block['txRoot'] ?? $block['tx_root'] ?? 'N/A',
|
||
'constitutionLayer'=> $constitution,
|
||
'constitutionText' => $constitution ? '已激活' : '未激活',
|
||
'constitutionBadge'=> $constitution ? 'bg-success' : 'bg-secondary',
|
||
'isHeartbeat' => $isHeartbeat,
|
||
'blockType' => $blockType,
|
||
'blockTypeLabel' => $blockTypeLabel,
|
||
'blockTypeNote' => $blockTypeNote,
|
||
'blockTypeBadge' => $isHeartbeat ? 'warning text-dark' : 'info text-dark',
|
||
'confirmations' => $block['confirmations'] ?? 0,
|
||
'dataSource' => $block['dataSource'] ?? 'CBPP-Node-9545',
|
||
'transactions' => $block['transactions'] ?? [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 状态徽章 CSS 类
|
||
*/
|
||
private function statusBadge(string $status): string
|
||
{
|
||
return match (strtolower($status)) {
|
||
'active', 'connected', 'running', 'online' => 'bg-success',
|
||
'syncing', 'pending', 'starting' => 'bg-warning text-dark',
|
||
'error', 'failed', 'offline', 'stopped' => 'bg-danger',
|
||
default => 'bg-secondary',
|
||
};
|
||
}
|
||
}
|