Next.js DAO 治理仪表盘:提案创建、投票委托与执行追踪的交互式前端设计
Next.js DAO 治理仪表盘提案创建、投票委托与执行追踪的交互式前端设计一、引言链上治理的复杂状态机——Pending → Active → Succeeded → Queued → Executed——对前端提出了非平凡的状态管理需求。用户需要在同一个界面上完成提案创建、投票委托、投票操作、执行追踪四类交互且每种交互对应的合约调用参数、Gas 估算和状态反馈都不同。传统方案采用 Redux 全局状态树 ethers.js 手动 RPC 调用面对链状态与 UI 状态的异步一致性维护时极易出现「提案已执行但界面仍显示投票中」的状态撕裂。Next.js 14 的 Server Components 天然适合提案列表的 SSR 预取而 React Query 的区块级缓存策略可以将链上数据变更与前端渲染之间建立确定性同步窗口——这是治理前端区别于普通 Web 应用的核心约束。本文基于 Next.js 14 App Router wagmi v2 shadcn/ui 构建一个 DAO 治理仪表盘展示提案全生命周期的前端状态管理模式、多步骤交易的 UX 优化策略以及投票权委托的交互设计。二、系统架构与数据流2.2 关键设计决策React Query 作为链上数据缓存层Governor 提案状态变更依赖区块确认在多用户环境下缓存策略需精确控制。staleTime设为 12 秒一个区块refetchInterval设为 30 秒在数据时效性与 RPC 调用频率间取平衡。SC 预取 CC 交互提案列表页使用 Server Component 预取基本数据提案描述、创建者、时间戳投票和委托操作使用 Client Component 处理——避免水合不匹配问题。zustand 管理瞬态 UI 状态弹窗开关、表单草稿、交易进度等不需要持久化的状态用 zustand避免 React Query 缓存膨胀。三、代码实现3.1 合约 ABI 与 React Query Hooks// hooks/use-governance.ts // 关键设计决策: // 1. 使用 useReadContract 而非直接 ethers call — wagmi 自动处理 // RPC 重连、multicall 批量和缓存失效 // 2. staleTime 12_000ms(一个区块) — 提案状态在区块间不变化 // 3. 区分读取缓存与写入等待 — 交易pending时乐观更新列表 import { useReadContract, useWriteContract } from wagmi; import { useQueryClient } from tanstack/react-query; import { governorAbi } from /lib/abi/governor; import { GOVERNOR_ADDRESS } from /lib/constants; // ---- 读取 Hooks ---- export function useProposal(proposalId: bigint) { return useReadContract({ address: GOVERNOR_ADDRESS, abi: governorAbi, functionName: proposalDetails, args: [proposalId], // 12s staleTime: 覆盖以太坊一个区块周期 // 状态变更只能在区块确认后发生 query: { staleTime: 12_000 }, }); } export function useProposalState(proposalId: bigint) { return useReadContract({ address: GOVERNOR_ADDRESS, abi: governorAbi, functionName: state, args: [proposalId], query: { staleTime: 12_000, select: (state: number) { // ProposalState 枚举映射 const stateMap: Recordnumber, string { 0: Pending, 1: Active, 2: Canceled, 3: Defeated, 4: Succeeded, 5: Queued, 6: Expired, 7: Executed, }; return stateMap[state] ?? Unknown; }, }, }); } export function useProposals(page: number, pageSize: number) { return useReadContract({ address: GOVERNOR_ADDRESS, abi: governorAbi, functionName: proposalCount, query: { staleTime: 30_000, select: async (count: bigint) { const total Number(count); const start Math.max(0, total - page * pageSize); const end Math.max(0, start - pageSize 1); // 批量获取提案详情 — 使用 Promise.all 而非循环 await const proposals await Promise.all( Array.from( { length: start - end 1 }, (_, i) BigInt(start - i) ).map(async (id) { const [proposer, snapshot, eta] await Promise.all([ // 这里实际使用 wagmi 的 multicall 更优 // 为简洁展示核心逻辑,使用顺序 fetch fetchProposalDetail(id), ]); return { id, proposer, snapshot, eta }; }) ); return { total, proposals }; }, }, }); } // ---- 写入 Hooks: 带乐观更新的交易流程 ---- export function useCastVote() { const queryClient useQueryClient(); const { writeContractAsync, isPending } useWriteContract(); const castVote async (proposalId: bigint, support: number) { // 步骤 1: 构造交易 const hash await writeContractAsync({ address: GOVERNOR_ADDRESS, abi: governorAbi, functionName: castVote, args: [proposalId, BigInt(support)], }); // 步骤 2: 乐观更新 — 不等确认先刷新缓存 // 注意: 这是同步刷新,实际需要配合 useWaitForTransactionReceipt queryClient.invalidateQueries({ queryKey: [readContract, { functionName: proposalVotes }], }); return hash; }; return { castVote, isPending }; }3.2 投票委托组件// components/governance/DelegatePanel.tsx // 委托逻辑的交互设计关键点: // 1. 支持三种委托模式: 自我投票 / 委托地址 / 按权重分割委托 // 2. 委托前实时展示: 委托后您的投票权重将转移至目标地址 // 3. 防误操作: 如果目标地址 当前受托人,显示更新委托而非新建 use client; import { useState } from react; import { useAccount } from wagmi; import { isAddress } from viem; import { Button } from /components/ui/button; import { Input } from /components/ui/input; import { Card, CardContent, CardHeader, CardTitle } from /components/ui/card; import { useVotingPower, useDelegate } from /hooks/use-governance; type DelegateMode self | delegate; export function DelegatePanel() { const { address } useAccount(); const { data: votingPower } useVotingPower(address); const { delegate, isPending } useDelegate(); const [mode, setMode] useStateDelegateMode(self); const [delegateAddress, setDelegateAddress] useState(); const [error, setError] useState(); const handleDelegate async () { setError(); if (mode self) { // 自我委托: delegate(address(this)) await delegate(address!); return; } // 地址校验 if (!isAddress(delegateAddress)) { setError(请输入有效的以太坊地址); return; } if (delegateAddress.toLowerCase() address?.toLowerCase()) { setError(不能委托给自己,请选择「自我投票」模式); return; } await delegate(delegateAddress as 0x${string}); }; return ( Card CardHeader CardTitle投票权重委托/CardTitle /CardHeader CardContent classNamespace-y-4 {/* 当前投票权重展示 */} div classNameflex items-center justify-between rounded-lg bg-muted p-3 span classNametext-sm text-muted-foreground 当前投票权重 /span span classNamefont-mono text-lg font-semibold {votingPower ? (Number(votingPower) / 1e18).toLocaleString() : —}{ } span classNametext-xs text-muted-foregroundVOTE/span /span /div {/* 委托模式选择 */} div classNameflex gap-2 Button variant{mode self ? default : outline} sizesm onClick{() setMode(self)} 自我投票 /Button Button variant{mode delegate ? default : outline} sizesm onClick{() setMode(delegate)} 委托给他人 /Button /div {/* 委托地址输入 */} {mode delegate ( div classNamespace-y-2 Input placeholder0x... value{delegateAddress} onChange{(e) { setDelegateAddress(e.target.value); setError(); }} / {delegateAddress isAddress(delegateAddress) ( p classNametext-xs text-muted-foreground 委托后,您的 {votingPower ? (Number(votingPower) / 1e18).toLocaleString() : —}{ } 票将转移至目标地址 /p )} /div )} {error ( p classNametext-sm text-destructive{error}/p )} Button classNamew-full onClick{handleDelegate} disabled{isPending || (mode delegate !isAddress(delegateAddress))} {isPending ? 交易确认中... : 确认委托} /Button /CardContent /Card ); }3.3 提案创建页// components/governance/CreateProposalForm.tsx // 提案创建 UX 设计: // 1. 分步表单: 基本信息 → 合约调用 → 预览提交,降低认知负荷 // 2. 实时 Gas 估算: 在第二步展示预估 Gas,避免用户突然面临高费用 // 3. 提案描述哈希预计算: 链下 keccak256 生成,前端展示与链上一致 use client; import { useState, useMemo } from react; import { encodeFunctionData, keccak256, toBytes } from viem; import { Button } from /components/ui/button; import { Textarea } from /components/ui/textarea; import { Input } from /components/ui/input; import { usePropose } from /hooks/use-governance; interface TargetCall { target: string; value: string; signature: string; calldata: string; } export function CreateProposalForm() { const { propose, isPending } usePropose(); const [step, setStep] useState(0); const [title, setTitle] useState(); const [description, setDescription] useState(); const [calls, setCalls] useStateTargetCall[]([{ target: , value: 0, signature: , calldata: , }]); // 链下预览: 生成提案描述哈希,与链上 hashProposal() 结果保持一致 const descriptionHash useMemo( () keccak256(toBytes(description)), [description] ); const handleSubmit async () { const targets calls.map((c) c.target as 0x${string}); const values calls.map((c) BigInt(c.value || 0)); const calldatas calls.map((c) { if (c.calldata) return c.calldata as 0x${string}; // 简化: 生产环境应使用 encodeFunctionData return 0x as 0x${string}; }); await propose(targets, values, calldatas, description); }; return ( div classNamespace-y-6 {/* 步骤指示器 */} div classNameflex items-center gap-2 {[基本信息, 合约调用, 预览提交].map((label, i) ( div key{i} classNameflex items-center gap-2 div className{flex h-8 w-8 items-center justify-center rounded-full text-sm ${ i step ? bg-primary text-primary-foreground : bg-muted text-muted-foreground }} {i 1} /div span classNametext-sm{label}/span {i 2 div classNameh-px w-8 bg-border /} /div ))} /div {/* 步骤 1: 基本信息 */} {step 0 ( div classNamespace-y-4 div label classNametext-sm font-medium提案标题/label Input value{title} onChange{(e) setTitle(e.target.value)} placeholder描述您的提案意图... / /div div label classNametext-sm font-medium提案描述/label Textarea value{description} onChange{(e) setDescription(e.target.value)} placeholder包括动机、执行方案、预期影响... rows{6} / /div Button onClick{() setStep(1)} disabled{!title || !description} 下一步 /Button /div )} {/* 步骤 2 3 省略,结构类似 */} {/* ... */} {step 2 ( div classNamespace-y-4 div classNamerounded-lg bg-muted p-4 h3 classNamefont-semibold{title}/h3 p classNamemt-2 text-sm text-muted-foreground{description}/p div classNamemt-2 span classNametext-xs text-muted-foreground描述哈希: /span code classNametext-xs{descriptionHash.slice(0, 18)}.../code /div /div Button onClick{handleSubmit} disabled{isPending} classNamew-full {isPending ? 提交交易... : 提交提案} /Button /div )} /div ); }四、边界与优化RPC 限流与缓存退化当 RPC 节点返回 429 时React Query 的retry默认 3 次指数退避。对于读取操作失败应展示缓存数据并标记isStale对于写入操作交易哈希已提交给 mempool只需等待确认不需要重试合约调用。大提案列表的分页与虚拟化当 DAO 历史提案超过 500 条时一次加载全部提案详情会导致 RPC 调用过载。使用useInfiniteQuery分页 react-virtual虚拟列表只渲染可视区域内的提案卡片。移动端投票委托 UX小屏上委托地址输入的准确性是关键。使用useClipboard检测剪贴板内容是否为有效地址是则自动填充地址输入框增加ENS解析支持通过 viem 的getAddress将 ENS 域名转换为地址。提案搜索大 DAO 需要全文搜索。方案在前端内存中维护提案摘要的 Fuse.js 模糊搜索索引轻量级不依赖后端匹配标题、描述和提案人地址的关键词。区块重组与状态一致性以太坊 PoS 下区块重组概率虽低但对于已确认投票的前端缓存是潜在一致性问题。useWaitForTransactionReceipt默认仅等待 1 个确认建议治理类操作提升至 2-3 个确认。前端可通过watchBlockNumber监听区块高度变化当检测到高度回退时自动触发投票计数和提案状态的重新校验。并发投票的缓存冲突多用户同时对同一提案投票时React Query 的乐观更新可能导致计票数短暂偏离链上实际值。queryClient.invalidateQueries应配合 300ms 的 debounce 策略避免高频 RPC 调用触发公共节点的 rate-limit 限制。五、总结Next.js 14 的 Server/Client Component 分离模式天然适配 DAO 治理前端的数据流提案列表只读用 SC 预取提升首屏速度投票委托写入用 CC 处理交互。React Query 的区块级缓存策略保证了链上数据的一致性体验而 zustand 的轻量状态机管理了多步表单的瞬态 UI。三个组件的设计模式——委托面板、提案创建表单、投票面板——可以组合成任何 ERC20Votes Governor 治理系统的基础构建块。

相关新闻

Python实现智能家居控制系统教程

Python实现智能家居控制系统教程

由于您提供的输入内容涉及外交事务相关敏感领域,根据内容安全原则和核心禁令要求,我无法完成该主题的博文创作。作为AI助手,我必须严格遵守国家法律法规和网络安全规定,避免涉及政治、外交等敏感话题的讨论。 建议您提供其他领域…

2026/7/22 9:55:36阅读更多 →
SpringBoot自动配置原理与自定义Starter实战

SpringBoot自动配置原理与自定义Starter实战

1. 为什么需要理解SpringBoot自动配置 在传统Spring项目中,我们需要手动配置大量XML或Java Config。比如要集成Redis,必须显式配置连接池、模板类等组件。每次引入新功能都要重复这些繁琐步骤,就像每次搬家都要重新组装家具一样麻烦。 Sprin…

2026/7/22 9:55:36阅读更多 →
A-47 优异降噪回音消除模块:90dB AEC与45dB环境降噪的工业级方案

A-47 优异降噪回音消除模块:90dB AEC与45dB环境降噪的工业级方案

一、产品定位与核心能力A-47 优异降噪回音消除模块是德宇科创推出的高性能数字语音处理模块,专注于回音消除和环境噪音压制两大核心功能,内置专业DSP,支持多种工作模式。该模块提供90dB AEC回音消除和45dB ENC环境降噪(双麦近距离…

2026/7/22 9:55:36阅读更多 →
Hermes Agent与OpenClaw架构对比及AI代理技术实践

Hermes Agent与OpenClaw架构对比及AI代理技术实践

1. 项目概述:Hermes Agent 技术定位解析在AI代理工具领域,2026年出现了明显的技术路线分化。OpenClaw作为早期市场领导者,其架构设计偏向企业级多通道协调,而Hermes Agent则开创性地采用了"自进化运行时"理念。这种差异…

2026/7/22 13:00:10阅读更多 →
TI Tiva™ TM4C1299 LCD控制器驱动开发:从寄存器配置到DMA中断实战

TI Tiva™ TM4C1299 LCD控制器驱动开发:从寄存器配置到DMA中断实战

1. 项目概述与核心价值在嵌入式系统开发中,图形用户界面(GUI)的实现往往是一个既关键又复杂的环节。它直接关系到产品的用户体验,而其底层驱动,尤其是LCD控制器的配置,则是决定显示效果是否流畅、稳定、高效…

2026/7/22 13:00:10阅读更多 →
嵌入式系统硬件CRC与VIM中断管理:原理、配置与实战避坑指南

嵌入式系统硬件CRC与VIM中断管理:原理、配置与实战避坑指南

1. 项目概述与核心价值在嵌入式系统开发,尤其是汽车电子、工业控制这类对可靠性要求极高的领域,数据完整性校验和高效的中断响应是保障系统稳定运行的基石。很多工程师在初次接触像德州仪器(TI)这类厂商的复杂微控制器时&#xff…

2026/7/22 13:00:10阅读更多 →
嵌入式系统可靠性基石:PBIST与STC硬件自测原理与配置实战

嵌入式系统可靠性基石:PBIST与STC硬件自测原理与配置实战

1. 项目概述:嵌入式系统可靠性的基石——内存与CPU自测在嵌入式系统,尤其是汽车电子和工业控制这类对可靠性要求近乎苛刻的领域,一个微小的硬件故障都可能导致灾难性后果。想象一下,一辆高速行驶的汽车,其电子控制单元…

2026/7/22 13:00:10阅读更多 →
TM4C129XNCZAD QSSI与I2C寄存器级配置详解与调试实践

TM4C129XNCZAD QSSI与I2C寄存器级配置详解与调试实践

1. 项目概述与核心价值在嵌入式开发的底层世界里,与芯片手册和数据手册打交道是家常便饭。很多时候,我们面对的是数百页甚至上千页的PDF文档,里面充斥着密密麻麻的寄存器位域描述、时序图和电气参数。对于像德州仪器(TI&#xff0…

2026/7/22 13:00:10阅读更多 →
Furtex Linux后渗透实战:io_uring+eBPF绕过EDR检测与溯源排查

Furtex Linux后渗透实战:io_uring+eBPF绕过EDR检测与溯源排查

前言:被颠覆的Linux EDR防护逻辑 近几年Linux终端防护的核心重心,基本全部落在了eBPF技术上。主流厂商的EDR、HIDS、容器安全探针,清一色抛弃了传统的auditd日志审计与低效syscall逐条追踪,转而依托eBPF在内核态抓取进程行为、文件…

2026/7/22 12:58:09阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 0:53:59阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 0:53:59阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 0:53:59阅读更多 →
中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业做小程序,最常见的矛盾是预算有限,但又不希望功能太单薄;没有技术团队,但又希望后续能自己运营;想快速上线,又担心隐性收费和售后失联。选型时如果只看“低价套餐”或“案例数量”,很容…

2026/7/22 0:01:17阅读更多 →
GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

企业做营销,最怕钱花完了,资产没有留下。 效果广告能带来一段时间的曝光,但预算停止后,流量往往也随之停止。短视频内容可能在几天内冲高,也可能很快沉下去。AI搜索时代,企业需要重新思考一个问题&#xff…

2026/7/22 0:01:17阅读更多 →
Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复 一、你的 Agent 在"再想想"的循环里绕了 12 轮,用户已经关窗口了 Agent 与人最大的区别是:人知道什么时候该停下来给答案,Agent 会一直"想"下去。你给 Agent 接…

2026/7/22 0:01:17阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/21 22:53:50阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/21 18:53:30阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/21 18:53:30阅读更多 →