feat(区块链浏览器): 实现区块详情弹窗和中文界面优化

添加区块详情弹窗功能,支持点击区块查看详细信息
优化导航菜单样式,添加活动状态指示器
将界面文本全面中文化并优化交易类型显示
添加区块列表动画效果提升用户体验
This commit is contained in:
祀梦
2026-01-11 23:14:08 +08:00
parent 52f0ab6366
commit d22b65f6d3
7 changed files with 670 additions and 50 deletions

View File

@@ -5,17 +5,49 @@
<div class="max-w-7xl mx-auto px-4 h-full flex items-center justify-between">
<div class="flex items-center space-x-2">
<div class="w-6 h-6 border-2 border-blue-500 rounded animate-spin-slow"></div>
<span class="text-xl font-bold text-white">FISCO BCOS Explorer</span>
<span class="px-2 py-0.5 rounded bg-blue-500/10 text-blue-400 text-xs border border-blue-500/20">Testnet</span>
<span class="text-xl font-bold text-white">FISCO BCOS 区块链浏览器</span>
<span class="px-2 py-0.5 rounded bg-blue-500/10 text-blue-400 text-xs border border-blue-500/20">测试网</span>
</div>
<nav class="flex space-x-6 text-sm items-center">
<router-link to="/" class="text-slate-400 hover:text-white flex items-center px-3 py-1 rounded border border-slate-700 hover:border-slate-500 transition-colors">
<span class="mr-1"></span> Mainnet
<nav class="flex space-x-6 text-sm items-center h-full">
<router-link to="/" class="text-slate-400 hover:text-white flex items-center px-3 py-1 rounded border border-slate-700 hover:border-slate-500 transition-colors mr-2">
<span class="mr-1"></span> 返回首页
</router-link>
<router-link
to="/blockchain/explorer"
class="transition-colors hover:text-white h-full flex items-center px-2 relative group"
active-class="text-white font-bold"
>
概览
<div class="absolute bottom-0 left-0 w-full h-0.5 bg-blue-500 scale-x-0 group-hover:scale-x-100 transition-transform origin-center active-indicator"></div>
</router-link>
<router-link
to="/blockchain/blocks"
class="transition-colors hover:text-white h-full flex items-center px-2 relative group"
active-class="text-white font-bold"
>
区块
<div class="absolute bottom-0 left-0 w-full h-0.5 bg-blue-500 scale-x-0 group-hover:scale-x-100 transition-transform origin-center active-indicator"></div>
</router-link>
<router-link
to="/blockchain/transactions"
class="transition-colors hover:text-white h-full flex items-center px-2 relative group"
active-class="text-white font-bold"
>
交易
<div class="absolute bottom-0 left-0 w-full h-0.5 bg-blue-500 scale-x-0 group-hover:scale-x-100 transition-transform origin-center active-indicator"></div>
</router-link>
<router-link
to="/blockchain/contracts"
class="transition-colors hover:text-white h-full flex items-center px-2 relative group"
active-class="text-white font-bold"
>
合约
<div class="absolute bottom-0 left-0 w-full h-0.5 bg-blue-500 scale-x-0 group-hover:scale-x-100 transition-transform origin-center active-indicator"></div>
</router-link>
<router-link to="/blockchain/explorer" class="text-white border-b-2 border-blue-500 pb-5 pt-5">概览</router-link>
<a href="#" class="hover:text-white transition-colors">区块</a>
<a href="#" class="hover:text-white transition-colors">交易</a>
<a href="#" class="hover:text-white transition-colors">合约</a>
</nav>
</div>
</header>
@@ -38,4 +70,8 @@
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.router-link-active .active-indicator {
transform: scaleX(1);
}
</style>

View File

@@ -68,6 +68,21 @@ const routes = [
path: 'explorer',
name: 'BlockchainExplorer',
component: () => import('../views/blockchain/Explorer.vue')
},
{
path: 'blocks',
name: 'BlockchainBlocks',
component: () => import('../views/blockchain/Blocks.vue')
},
{
path: 'transactions',
name: 'BlockchainTransactions',
component: () => import('../views/blockchain/Transactions.vue')
},
{
path: 'contracts',
name: 'BlockchainContracts',
component: () => import('../views/blockchain/Contracts.vue')
}
]
}

110
src/stores/blockchain.js Normal file
View File

@@ -0,0 +1,110 @@
import { defineStore } from 'pinia'
import { ref, onMounted, onUnmounted } from 'vue'
export const useBlockchainStore = defineStore('blockchain', () => {
const stats = ref([
{ label: '区块高度', value: '8,441', icon: '📦' },
{ label: '交易总数', value: '142,506', icon: '🔗' },
{ label: '活跃节点', value: '21/21', icon: '🌐' },
{ label: '平均出块时间', value: '2.1s', icon: '⚡' }
])
const blocks = ref([
{ height: 8435, hash: '0x12e6...5027', txs: 5, time: '14s', details: [] },
{ height: 8436, hash: '0xb844...0080', txs: 4, time: '12s', details: [] },
{ height: 8437, hash: '0x5532...581a', txs: 2, time: '10s', details: [] },
{ height: 8438, hash: '0xc34d...7dbd', txs: 4, time: '8s', details: [] },
{ height: 8439, hash: '0xd31b...cc3b', txs: 5, time: '6s', details: [] },
{ height: 8440, hash: '0x0d90...9885', txs: 1, time: '4s', details: [] },
{ height: 8441, hash: '0xa592...8b0d', txs: 6, time: '2s', details: [] },
])
const transactions = ref([
{ hash: '0x7d21...4e1a', type: 'Evidence', from: 'Robot-SN088', to: 'Contract:Main', time: '10s' },
{ hash: '0x9b32...1f82', type: 'TokenTransfer', from: 'Agency-Node', to: 'Robot-SN012', time: '15s' },
{ hash: '0x1c84...9a23', type: 'Alert', from: 'Robot-SN045', to: 'Admin-Node', time: '20s' },
{ hash: '0x4e55...bb12', type: 'Evidence', from: 'Robot-SN102', to: 'Contract:Main', time: '25s' },
{ hash: '0x2a91...cc84', type: 'ContractDeploy', from: 'Admin-Node', to: 'Blockchain-Net', time: '30s' },
{ hash: '0x8f32...dd91', type: 'TokenTransfer', from: 'Robot-SN099', to: 'Agency-Node', time: '35s' },
])
const generateRandomTxs = (count) => {
const types = ['Evidence', 'TokenTransfer', 'Alert', 'ContractDeploy']
return Array.from({ length: count }).map(() => ({
hash: '0x' + Math.random().toString(16).substring(2, 10) + '...',
type: types[Math.floor(Math.random() * types.length)],
from: `Robot-SN0${Math.floor(Math.random() * 999)}`,
to: Math.random() > 0.5 ? 'Contract:Main' : 'Agency-Node',
status: 'Success'
}))
}
// 初始化区块详情
blocks.value.forEach(b => {
if (!b.details || b.details.length === 0) {
b.details = generateRandomTxs(b.txs)
}
})
let timer = null
const startSimulation = () => {
if (timer) return
timer = setInterval(() => {
// 1. 生成新区块
const currentHeight = parseInt(stats.value[0].value.replace(',', ''))
const newHeight = currentHeight + 1
const txCountInBlock = Math.floor(Math.random() * 6) + 1
// 2. 更新统计数据
stats.value[0].value = newHeight.toLocaleString()
const currentTotalTxs = parseInt(stats.value[1].value.replace(',', ''))
stats.value[1].value = (currentTotalTxs + txCountInBlock).toLocaleString()
// 3. 将新块从右侧推入
const newHash = '0x' + Math.random().toString(16).substring(2, 6) + '...' + Math.random().toString(16).substring(2, 6)
const newBlock = {
height: newHeight,
hash: newHash,
txs: txCountInBlock,
time: '0s',
details: generateRandomTxs(txCountInBlock)
}
blocks.value.push(newBlock)
if (blocks.value.length > 20) blocks.value.shift()
// 4. 同步生成交易记录
const newTxs = newBlock.details.map(d => ({
...d,
time: '0s'
}))
transactions.value.unshift(...newTxs)
if (transactions.value.length > 50) transactions.value.splice(50)
// 5. 更新存量时间
blocks.value.forEach((b) => {
const seconds = parseInt(b.time) + 2
b.time = `${seconds}s`
})
transactions.value.forEach((tx) => {
const seconds = parseInt(tx.time) + 2
tx.time = `${seconds}s`
})
}, 2100)
}
const stopSimulation = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
return {
stats,
blocks,
transactions,
startSimulation,
stopSimulation
}
})

View File

@@ -0,0 +1,150 @@
<template>
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-white flex items-center">
<span class="mr-2 text-blue-500">📦</span> 区块列表
</h2>
<div class="text-xs text-slate-500 bg-slate-800/50 px-3 py-1 rounded-full border border-slate-700">
2.1s 产生一个新块
</div>
</div>
<!-- Stats Mini -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div v-for="stat in stats" :key="stat.label" class="bg-[#1e293b]/30 border border-slate-700/50 p-3 rounded-lg">
<div class="text-[10px] text-slate-500 uppercase">{{ stat.label }}</div>
<div class="text-sm font-mono text-slate-200">{{ stat.value }}</div>
</div>
</div>
<!-- Blocks Table -->
<div class="bg-[#1e293b]/50 border border-slate-700 rounded-xl overflow-hidden">
<table class="w-full text-left text-sm text-slate-400">
<thead class="bg-slate-800/50 text-xs uppercase text-slate-500">
<tr>
<th class="px-6 py-4">高度</th>
<th class="px-6 py-4">区块哈希</th>
<th class="px-6 py-4">交易数</th>
<th class="px-6 py-4">生成时间</th>
<th class="px-6 py-4">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-700/50">
<tr v-for="block in sortedBlocks" :key="block.height" class="hover:bg-slate-700/20 transition-colors group">
<td class="px-6 py-4">
<span class="text-blue-400 font-bold">#{{ block.height }}</span>
</td>
<td class="px-6 py-4 font-mono text-xs text-slate-400 group-hover:text-slate-200 transition-colors">
{{ block.hash }}
</td>
<td class="px-6 py-4">
<span class="px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-xs border border-blue-500/20">
{{ block.txs }} 交易
</span>
</td>
<td class="px-6 py-4 text-xs text-slate-500 italic">
{{ block.time }}
</td>
<td class="px-6 py-4">
<button @click="openBlockDetails(block)" class="text-xs text-blue-500 hover:text-blue-400 font-medium">查看详情</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination Simulation -->
<div class="flex justify-center mt-6">
<div class="flex space-x-2">
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 cursor-not-allowed"></button>
<button class="w-8 h-8 rounded border border-blue-500/50 bg-blue-500/10 flex items-center justify-center text-blue-400 font-bold">1</button>
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 hover:border-slate-500">2</button>
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 hover:border-slate-500">3</button>
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 hover:border-slate-500"></button>
</div>
</div>
<!-- Reuse Block Details Modal from Explorer -->
<div v-if="showDetails" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="showDetails = false"></div>
<div class="relative w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-300">
<!-- Modal Content (same as Explorer.vue) -->
<div class="p-6 border-b border-slate-800 flex justify-between items-center bg-slate-800/50">
<div>
<h3 class="text-xl font-bold text-white">区块 #{{ selectedBlock.height }} 详情</h3>
<p class="text-xs text-slate-400 font-mono mt-1">{{ selectedBlock.hash }}</p>
</div>
<button @click="showDetails = false" class="text-slate-400 hover:text-white transition-colors p-2 text-2xl">×</button>
</div>
<div class="p-6 max-h-[60vh] overflow-y-auto">
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-slate-800/30 p-4 rounded-xl border border-slate-700/50">
<span class="text-xs text-slate-500 block mb-1">包含交易数</span>
<span class="text-lg font-bold text-blue-400">{{ selectedBlock.txs }}</span>
</div>
<div class="bg-slate-800/30 p-4 rounded-xl border border-slate-700/50">
<span class="text-xs text-slate-500 block mb-1">生成时间</span>
<span class="text-lg font-bold text-slate-300">{{ selectedBlock.time }} </span>
</div>
</div>
<h4 class="text-sm font-semibold text-slate-300 mb-3 px-1">交易列表</h4>
<div class="space-y-2">
<div v-for="tx in selectedBlock.details" :key="tx.hash" class="flex items-center justify-between p-3 bg-slate-800/50 border border-slate-700/30 rounded-lg">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded bg-blue-500/10 flex items-center justify-center text-blue-400 text-[10px] font-bold">
{{ tx.type.charAt(0) }}
</div>
<div>
<div class="text-[10px] font-mono text-slate-300">{{ tx.hash }}</div>
<div class="text-[10px] text-slate-500">{{ tx.from }} {{ tx.to }}</div>
</div>
</div>
<div class="flex flex-col items-end">
<span class="px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-[9px] border border-blue-500/20 mb-1">
{{ translateType(tx.type) }}
</span>
<span class="text-[9px] text-green-400 flex items-center">已确认</span>
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-slate-800 bg-slate-800/30 flex justify-end">
<button @click="showDetails = false" class="px-6 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm transition-colors">关闭窗口</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useBlockchainStore } from '../../stores/blockchain'
const store = useBlockchainStore()
const stats = computed(() => store.stats)
const blocks = computed(() => store.blocks)
const sortedBlocks = computed(() => [...blocks.value].reverse())
const selectedBlock = ref(null)
const showDetails = ref(false)
const openBlockDetails = (block) => {
selectedBlock.value = block
showDetails.value = true
}
const translateType = (type) => {
const map = {
'Evidence': '行为存证',
'TokenTransfer': '服务结算',
'Alert': '异常告警',
'ContractDeploy': '合约部署'
}
return map[type] || type
}
onMounted(() => {
store.startSimulation()
})
</script>

View File

@@ -0,0 +1,99 @@
<template>
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-white flex items-center">
<span class="mr-2 text-blue-500">📜</span> 合约管理
</h2>
<div class="text-xs text-slate-500 bg-slate-800/50 px-3 py-1 rounded-full border border-slate-700">
已部署智能合约
</div>
</div>
<!-- Contracts Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div v-for="contract in contracts" :key="contract.address"
class="bg-[#1e293b]/50 border border-slate-700 rounded-xl p-6 hover:border-blue-500/50 transition-all group">
<div class="flex justify-between items-start mb-4">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center text-blue-400">
<span class="text-xl">📄</span>
</div>
<div>
<h3 class="text-white font-bold">{{ contract.name }}</h3>
<p class="text-[10px] text-slate-500 font-mono">{{ contract.address }}</p>
</div>
</div>
<span class="px-2 py-1 rounded bg-green-500/10 text-green-400 text-[10px] border border-green-500/20">运行中</span>
</div>
<div class="space-y-3 mb-6">
<div class="flex justify-between text-xs">
<span class="text-slate-500">版本</span>
<span class="text-slate-300">{{ contract.version }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-slate-500">部署时间</span>
<span class="text-slate-300">{{ contract.deployTime }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-slate-500">总调用次数</span>
<span class="text-blue-400 font-mono">{{ contract.calls }}</span>
</div>
</div>
<div class="flex space-x-2">
<button class="flex-1 py-2 rounded-lg bg-slate-800 text-slate-300 text-xs hover:bg-slate-700 transition-colors">源代码</button>
<button class="flex-1 py-2 rounded-lg bg-blue-600 text-white text-xs hover:bg-blue-500 transition-colors">控制台</button>
</div>
</div>
</div>
<!-- Deployment Simulation -->
<div class="bg-blue-600/5 border border-blue-500/20 rounded-xl p-8 flex flex-col items-center justify-center text-center space-y-4">
<div class="w-12 h-12 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-400">
<span class="text-2xl">+</span>
</div>
<div>
<h4 class="text-white font-bold">部署新合约</h4>
<p class="text-xs text-slate-500 max-w-xs mt-1">支持 Solidity/WASM 智能合约部署到 FISCO BCOS 测试网</p>
</div>
<button class="px-8 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm transition-colors">开始部署</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const contracts = ref([
{
name: 'MainLogicContract',
address: '0x8f2a2435...9b1c',
version: 'v1.2.0',
deployTime: '2025-12-01',
calls: '45,892'
},
{
name: 'RobotIdentityManager',
address: '0x3d4e1289...1f9a',
version: 'v1.0.5',
deployTime: '2025-11-15',
calls: '12,406'
},
{
name: 'EvidenceStorage',
address: '0x7a8b5562...2c3d',
version: 'v2.1.0',
deployTime: '2025-12-10',
calls: '89,120'
},
{
name: 'SettlementService',
address: '0x1c84ee92...9a23',
version: 'v1.1.2',
deployTime: '2025-12-20',
calls: '5,230'
}
])
</script>

View File

@@ -9,21 +9,95 @@
</div>
<!-- Latest Blocks (Cube Stream) -->
<div class="relative h-48 bg-[#1e293b]/30 border border-slate-700 rounded-xl overflow-hidden flex items-center px-4 space-x-4">
<div class="absolute inset-0 bg-gradient-to-r from-[#0f172a] via-transparent to-[#0f172a] z-10 pointer-events-none"></div>
<div class="relative h-48 bg-[#1e293b]/30 border border-slate-700 rounded-xl overflow-hidden flex items-center">
<!-- 左侧淡化遮罩用于块滑出时的视觉效果 -->
<div class="absolute inset-y-0 left-0 w-32 bg-gradient-to-r from-[#0f172a] to-transparent z-20 pointer-events-none"></div>
<div v-for="block in blocks" :key="block.height"
class="min-w-[140px] h-32 bg-gradient-to-br from-blue-900 to-slate-900 border border-blue-500/30 rounded-lg p-3 flex flex-col justify-between transform transition-all hover:scale-105 hover:border-blue-400 cursor-pointer group shadow-lg shadow-blue-900/20">
<div class="flex justify-between items-start">
<span class="text-xs text-blue-300 font-mono">#{{ block.height }}</span>
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
<!-- 区块容器左对齐允许向右延伸 -->
<div class="flex-1 h-full flex items-center px-6 overflow-hidden">
<TransitionGroup
name="block-list"
tag="div"
class="flex space-x-4 items-center relative"
>
<div v-for="block in blocks" :key="block.height"
@click="openBlockDetails(block)"
class="min-w-[140px] h-32 bg-gradient-to-br from-blue-900/80 to-slate-900 border border-blue-500/30 rounded-lg p-3 flex flex-col justify-between transform transition-all hover:scale-105 hover:border-blue-400 cursor-pointer group shadow-lg shadow-blue-900/20">
<div class="flex justify-between items-start">
<span class="text-xs text-blue-300 font-mono">#{{ block.height }}</span>
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
</div>
<div class="text-[10px] text-slate-400 font-mono truncate">
{{ block.hash }}
</div>
<div class="text-xs text-white">
{{ block.txs }} 交易
<span class="block text-[10px] text-slate-500">{{ block.time }} </span>
</div>
</div>
</TransitionGroup>
</div>
</div>
<!-- Block Details Modal -->
<div v-if="showDetails" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="showDetails = false"></div>
<div class="relative w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-300">
<!-- Header -->
<div class="p-6 border-b border-slate-800 flex justify-between items-center bg-slate-800/50">
<div>
<h3 class="text-xl font-bold text-white">区块 #{{ selectedBlock.height }} 详情</h3>
<p class="text-xs text-slate-400 font-mono mt-1">{{ selectedBlock.hash }}</p>
</div>
<button @click="showDetails = false" class="text-slate-400 hover:text-white transition-colors p-2">
<span class="text-2xl">×</span>
</button>
</div>
<div class="text-[10px] text-slate-400 font-mono truncate">
{{ block.hash }}
<!-- Content -->
<div class="p-6 max-h-[60vh] overflow-y-auto">
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-slate-800/30 p-4 rounded-xl border border-slate-700/50">
<span class="text-xs text-slate-500 block mb-1">包含交易数</span>
<span class="text-lg font-bold text-blue-400">{{ selectedBlock.txs }}</span>
</div>
<div class="bg-slate-800/30 p-4 rounded-xl border border-slate-700/50">
<span class="text-xs text-slate-500 block mb-1">生成时间</span>
<span class="text-lg font-bold text-slate-300">{{ selectedBlock.time }} </span>
</div>
</div>
<h4 class="text-sm font-semibold text-slate-300 mb-3 px-1">交易列表</h4>
<div class="space-y-2">
<div v-for="tx in selectedBlock.details" :key="tx.hash"
class="flex items-center justify-between p-3 bg-slate-800/50 border border-slate-700/30 rounded-lg hover:border-blue-500/30 transition-colors">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded bg-blue-500/10 flex items-center justify-center text-blue-400 text-[10px] font-bold">
{{ tx.type.charAt(0) }}
</div>
<div>
<div class="text-[10px] font-mono text-slate-300">{{ tx.hash }}</div>
<div class="text-[10px] text-slate-500">{{ tx.from }} {{ tx.to }}</div>
</div>
</div>
<div class="flex flex-col items-end">
<span class="px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-[9px] border border-blue-500/20 mb-1">
{{ translateType(tx.type) }}
</span>
<span class="text-[9px] text-green-400 flex items-center">
<span class="w-1 h-1 rounded-full bg-green-400 mr-1"></span> 已确认
</span>
</div>
</div>
</div>
</div>
<div class="text-xs text-white">
{{ block.txs }} txs
<span class="block text-[10px] text-slate-500">{{ block.time }} ago</span>
<!-- Footer -->
<div class="p-4 border-t border-slate-800 bg-slate-800/30 flex justify-end">
<button @click="showDetails = false"
class="px-6 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium transition-colors">
关闭窗口
</button>
</div>
</div>
</div>
@@ -31,28 +105,28 @@
<!-- Transactions Table -->
<div class="bg-[#1e293b]/50 border border-slate-700 rounded-xl overflow-hidden">
<div class="px-6 py-4 border-b border-slate-700 flex justify-between items-center">
<h3 class="text-white font-bold">Latest Transactions</h3>
<button class="text-xs text-blue-400 hover:text-blue-300">View All ></button>
<h3 class="text-white font-bold">最新交易</h3>
<button class="text-xs text-blue-400 hover:text-blue-300">查看全部 ></button>
</div>
<table class="w-full text-left text-sm text-slate-400">
<thead class="bg-slate-800/50 text-xs uppercase">
<tr>
<th class="px-6 py-3">Tx Hash</th>
<th class="px-6 py-3">Type</th>
<th class="px-6 py-3">From</th>
<th class="px-6 py-3">To</th>
<th class="px-6 py-3">Status</th>
<th class="px-6 py-3">交易哈希</th>
<th class="px-6 py-3">类型</th>
<th class="px-6 py-3">发送方</th>
<th class="px-6 py-3">接收方</th>
<th class="px-6 py-3">状态</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-700">
<tr v-for="tx in txs" :key="tx.hash" class="hover:bg-slate-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-blue-400">{{ tx.hash }}</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded text-xs border" :class="getTypeClass(tx.type)">{{ tx.type }}</span>
<span class="px-2 py-1 rounded text-xs border" :class="getTypeClass(tx.type)">{{ translateType(tx.type) }}</span>
</td>
<td class="px-6 py-4 font-mono truncate max-w-[100px]">{{ tx.from }}</td>
<td class="px-6 py-4 font-mono truncate max-w-[100px]">{{ tx.to }}</td>
<td class="px-6 py-4 text-green-400 text-xs">Success</td>
<td class="px-6 py-4 text-green-400 text-xs">成功</td>
</tr>
</tbody>
</table>
@@ -61,33 +135,74 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useBlockchainStore } from '../../stores/blockchain'
const stats = ref([
{ label: 'Block Height', value: '12,403,921' },
{ label: 'Total Txs', value: '89,201,332' },
{ label: 'Nodes Active', value: '4/4' },
{ label: 'Avg Block Time', value: '1.02s' },
])
const store = useBlockchainStore()
const stats = computed(() => store.stats)
const blocks = computed(() => store.blocks)
const txs = computed(() => store.transactions.slice(0, 6))
const blocks = ref([
{ height: 12403921, hash: '0x8f2a...9b1c', txs: 12, time: '2s' },
{ height: 12403920, hash: '0x3d4e...1f9a', txs: 45, time: '3s' },
{ height: 12403919, hash: '0x7a8b...2c3d', txs: 8, time: '4s' },
{ height: 12403918, hash: '0x1c2d...3e4f', txs: 156, time: '5s' },
{ height: 12403917, hash: '0x5g6h...7i8j', txs: 23, time: '6s' },
])
// 区块详情弹窗逻辑
const selectedBlock = ref(null)
const showDetails = ref(false)
const txs = ref([
{ hash: '0x8f2a...9b1c', type: 'Evidence', from: 'Robot-082', to: 'Contract:Evidence' },
{ hash: '0x3d4e...1f9a', type: 'TokenTransfer', from: 'Family-User', to: 'Agency-Wallet' },
{ hash: '0x7a8b...2c3d', type: 'Alert', from: 'Robot-015', to: 'Contract:Alert' },
{ hash: '0x9c0d...1e2f', type: 'Evidence', from: 'Robot-099', to: 'Contract:Evidence' },
])
const openBlockDetails = (block) => {
selectedBlock.value = block
showDetails.value = true
}
const translateType = (type) => {
const map = {
'Evidence': '行为存证',
'TokenTransfer': '服务结算',
'Alert': '异常告警',
'ContractDeploy': '合约部署'
}
return map[type] || type
}
const getTypeClass = (type) => {
if (type === 'Evidence') return 'bg-blue-900/30 text-blue-300 border-blue-800'
if (type === 'Alert') return 'bg-red-900/30 text-red-300 border-red-800'
if (type === 'ContractDeploy') return 'bg-purple-900/30 text-purple-300 border-purple-800'
return 'bg-green-900/30 text-green-300 border-green-800'
}
onMounted(() => {
store.startSimulation()
})
onUnmounted(() => {
// 保持后台仿真运行,这样切换页面时数据是连续的
})
</script>
<style scoped>
.block-list-enter-active,
.block-list-leave-active {
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 关键:离开的元素设置为绝对定位,确保后续元素能平滑触发 move 动画实现整体滑动 */
.block-list-leave-active {
position: absolute;
}
/* 新块从右侧平滑插入 */
.block-list-enter-from {
opacity: 0;
transform: translateX(160px);
}
/* 旧块向左滑动并淡出 */
.block-list-leave-to {
opacity: 0;
transform: translateX(-160px);
}
/* 整体滑动核心:所有在位元素的移动过渡 */
.block-list-move {
transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-white flex items-center">
<span class="mr-2 text-blue-500">🔗</span> 交易列表
</h2>
<div class="text-xs text-slate-500 bg-slate-800/50 px-3 py-1 rounded-full border border-slate-700">
所有链上活动记录
</div>
</div>
<!-- Transactions Table -->
<div class="bg-[#1e293b]/50 border border-slate-700 rounded-xl overflow-hidden">
<table class="w-full text-left text-sm text-slate-400">
<thead class="bg-slate-800/50 text-xs uppercase text-slate-500">
<tr>
<th class="px-6 py-4">交易哈希</th>
<th class="px-6 py-4">类型</th>
<th class="px-6 py-4">发送方</th>
<th class="px-6 py-4">接收方</th>
<th class="px-6 py-4">状态</th>
<th class="px-6 py-4">时间</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-700/50">
<tr v-for="tx in transactions" :key="tx.hash" class="hover:bg-slate-700/20 transition-colors">
<td class="px-6 py-4 font-mono text-blue-400 text-xs">
{{ tx.hash }}
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded text-[10px] border" :class="getTypeClass(tx.type)">
{{ translateType(tx.type) }}
</span>
</td>
<td class="px-6 py-4 font-mono text-xs truncate max-w-[120px]" :title="tx.from">
{{ tx.from }}
</td>
<td class="px-6 py-4 font-mono text-xs truncate max-w-[120px]" :title="tx.to">
{{ tx.to }}
</td>
<td class="px-6 py-4">
<span class="flex items-center text-green-400 text-[10px]">
<span class="w-1 h-1 rounded-full bg-green-400 mr-1.5"></span> 成功
</span>
</td>
<td class="px-6 py-4 text-xs text-slate-500">
{{ tx.time }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination Simulation -->
<div class="flex justify-center mt-6">
<div class="flex space-x-2">
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 cursor-not-allowed"></button>
<button class="w-8 h-8 rounded border border-blue-500/50 bg-blue-500/10 flex items-center justify-center text-blue-400 font-bold">1</button>
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 hover:border-slate-500">2</button>
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 hover:border-slate-500">3</button>
<button class="w-8 h-8 rounded border border-slate-700 flex items-center justify-center text-slate-500 hover:border-slate-500"></button>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { useBlockchainStore } from '../../stores/blockchain'
const store = useBlockchainStore()
const transactions = computed(() => store.transactions)
const translateType = (type) => {
const map = {
'Evidence': '行为存证',
'TokenTransfer': '服务结算',
'Alert': '异常告警',
'ContractDeploy': '合约部署'
}
return map[type] || type
}
const getTypeClass = (type) => {
if (type === 'Evidence') return 'bg-blue-900/30 text-blue-300 border-blue-800'
if (type === 'Alert') return 'bg-red-900/30 text-red-300 border-red-800'
if (type === 'ContractDeploy') return 'bg-purple-900/30 text-purple-300 border-purple-800'
return 'bg-green-900/30 text-green-300 border-green-800'
}
onMounted(() => {
store.startSimulation()
})
</script>