48 小时极客原型:用 Viem 与 Tailwind 打造去中心化 DAO 投票治理全景看板
·
48 小时极客原型:用 Viem 与 Tailwind 打造去中心化 DAO 投票治理全景看板

在去中心化自治组织(DAO)的日常治理中,投票治理(Governor Alpha / Bravo / OpenZeppelin Governor)是决定国库资金调拨与协议升级路线的核心机制。
然而,传统的 Snapshot 或 Tally 页面在面对复杂提案时,存在诸多交互痛点:
- 无法直观看到当前赞成票(For)、反对票(Against)与弃权票(Abstain)在**法定门限(Quorum)**下的真实达成进度;
- 缺乏对参与投票的巨鲸成员地址的实时穿透分析。
在上周末的 48 小时极客冲刺中,我利用 Viem 链上治理合约探针、Next.js 14 App Router 与 Tailwind CSS 赛博朋克深色仪表盘,从零构建了一套“全链去中心化 DAO 投票治理全景看板”。
一、DAO 治理看板全栈系统架构拓扑
graph TD
ViemGovernor[Viem 治理探针: 读取 Governor 合约 state, proposalVotes, quorum] --> StateProcessor[数据清洗与 Quorum 达成率计算]
subgraph Tailwind 赛博风治理看板
StateProcessor --> QuorumBar[法定门限动态进度条 (Quorum Progress Bar)]
StateProcessor --> VoteBreakdown[三色投票占比环形/柱状矩阵 (For/Against/Abstain)]
StateProcessor --> WhaleList[巨鲸投票历史流水与 ENS 标签展示]
end
VoteBreakdown & QuorumBar --> ClientView[用户前端实时展示 (100% 声明式响应)]
二、DAO 治理链上数据读取与统计服务实现
// lib/daoGovernanceService.ts
import { createPublicClient, http, parseAbi, formatEther } from 'viem';
import { mainnet } from 'viem/chains';
const GOVERNOR_CONTRACT = '0xGovernorAddress...';
const governorAbi = parseAbi([
'function state(uint256 proposalId) external view returns (uint8)',
'function proposalVotes(uint256 proposalId) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes)',
'function quorum(uint256 blockNumber) external view returns (uint256)',
]);
export interface ProposalVoteStats {
proposalId: string;
title: string;
status: 'PENDING' | 'ACTIVE' | 'CANCELED' | 'DEFEATED' | 'SUCCEEDED' | 'QUEUED' | 'EXECUTED';
forVotes: number;
againstVotes: number;
abstainVotes: number;
totalVotes: number;
quorumNeeded: number;
quorumReachedPct: number;
forPct: number;
againstPct: number;
}
export async function fetchProposalGovernanceDetails(proposalId: bigint): Promise<ProposalVoteStats> {
const client = createPublicClient({ chain: mainnet, transport: http() });
// 模拟从链上并行读取投票状态
const forVotes = 1450000;
const againstVotes = 320000;
const abstainVotes = 50000;
const quorumNeeded = 1200000;
const total = forVotes + againstVotes + abstainVotes;
const quorumPct = Math.min(100, Math.round((forVotes / quorumNeeded) * 100));
return {
proposalId: proposalId.toString(),
title: 'CIP-42: 将协议国库 500,000 USDC 存入去中心化 AI 算力金库',
status: 'ACTIVE',
forVotes,
againstVotes,
abstainVotes,
totalVotes: total,
quorumNeeded,
quorumReachedPct: quorumPct,
forPct: Math.round((forVotes / total) * 100),
againstPct: Math.round((againstVotes / total) * 100),
};
}
三、React + Tailwind 赛博朋克治理看板界面实现
// components/DAOGovernanceHUD.tsx
'use client';
import React, { useEffect, useState } from 'react';
import { ProposalVoteStats, fetchProposalGovernanceDetails } from '@/lib/daoGovernanceService';
export function DAOGovernanceHUD() {
const [stats, setStats] = useState<ProposalVoteStats | null>(null);
useEffect(() => {
fetchProposalGovernanceDetails(42n).then(setStats);
}, []);
if (!stats) return <div className="h-64 bg-slate-900 animate-pulse rounded-3xl" />;
return (
<div className="max-w-3xl mx-auto p-8 bg-slate-950 border border-slate-800 rounded-3xl text-white shadow-2xl">
{/* 头部状态与标题 */}
<div className="flex justify-between items-start mb-6">
<div>
<div className="flex items-center gap-2">
<span className="text-[10px] px-2.5 py-0.5 bg-green-950 text-green-400 border border-green-500/40 rounded-full font-mono font-bold">
{stats.status}
</span>
<span className="text-xs text-slate-400 font-mono">PROPOSAL #{stats.proposalId}</span>
</div>
<h2 className="text-xl font-bold text-slate-100 mt-2">{stats.title}</h2>
</div>
</div>
{/* 法定门限达成度 Quorum 进度条 */}
<div className="mb-8 p-5 bg-slate-900/80 border border-slate-800 rounded-2xl">
<div className="flex justify-between items-center text-xs font-mono mb-2">
<span className="text-slate-400">法定有效门限 (Quorum Progress)</span>
<span className="text-cyan-300 font-bold">{stats.quorumReachedPct}% 已达成</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-3 overflow-hidden border border-slate-800">
<div
className="bg-cyan-400 h-3 rounded-full transition-all duration-500 shadow-[0_0_12px_#00f3ff]"
style={{ width: `${stats.quorumReachedPct}%` }}
/>
</div>
</div>
{/* 赞成 vs 反对 三色投票柱状比例 */}
<div className="space-y-4">
{/* 赞成 For */}
<div className="p-4 bg-slate-900/90 border border-green-500/30 rounded-2xl">
<div className="flex justify-between items-center text-xs mb-2">
<span className="text-green-400 font-bold flex items-center gap-1.5">
<span>🟢</span> 赞成 (FOR)
</span>
<span className="font-mono text-slate-300 font-bold">{stats.forVotes.toLocaleString()} 票 ({stats.forPct}%)</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden">
<div className="bg-green-500 h-2 rounded-full" style={{ width: `${stats.forPct}%` }} />
</div>
</div>
{/* 反对 Against */}
<div className="p-4 bg-slate-900/90 border border-red-500/30 rounded-2xl">
<div className="flex justify-between items-center text-xs mb-2">
<span className="text-red-400 font-bold flex items-center gap-1.5">
<span>🔴</span> 反对 (AGAINST)
</span>
<span className="font-mono text-slate-300 font-bold">{stats.againstVotes.toLocaleString()} 票 ({stats.againstPct}%)</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden">
<div className="bg-red-500 h-2 rounded-full" style={{ width: `${stats.againstPct}%` }} />
</div>
</div>
</div>
{/* 底部一键投票签名按钮 */}
<div className="mt-8 pt-6 border-t border-slate-800 flex gap-4">
<button className="flex-1 py-3 bg-green-600 hover:bg-green-500 rounded-xl font-bold text-xs transition">
投赞成票 (Vote For)
</button>
<button className="flex-1 py-3 bg-red-600 hover:bg-red-500 rounded-xl font-bold text-xs transition">
投反对票 (Vote Against)
</button>
</div>
</div>
);
}
四、48 小时极客原型调优复盘
- 快照区块(Snapshot Block)权重对齐:投票权不是根据当前余额计算,而是必须使用
getPastVotes(user, proposalSnapshotBlock)读取提案创建瞬间的历史代币快照,杜绝闪电贷借币操控投票; - 零布局偏移骨架屏支持:看板在初次加载时输出完全对齐的骨架结构,首屏加载极为丝滑。
用清晰直观的数据看板赋能去中心化民主治理,让社区共识在透明的数据流中凝聚。
更多推荐



所有评论(0)