|
本帖最后由 riyad 于 2025-3-20 21:47 编辑 ' P9 D! K6 K! I+ q/ P5 F
2 c+ t* [. ^) [ a
在前两篇教程中,我已经完成了红包智能合约的编写、编译和部署到 TON 测试网。今天,我们进入第三部分:开发一个前端界面,让用户可以直观地与红包合约交互。我使用 Vite + React + TypeScript 搭建了前端项目,并集成了 TON 钱包连接功能。以下是我的开发过程和完整代码,欢迎大家参考!: V: n; v" f! h- J M. D$ B: u8 ~
& _) e! T+ j m" {/ a前提条件
3 p# c' D/ f8 X! ?) J. j在开始之前,确保你已完成以下步骤:9 f) [, e8 m+ G$ P- h/ V
0 e, f$ y" V! t' [, ~! m8 L
- 完成前两部分:你已经编写、编译并部署了红包智能合约到 TON 测试网。
- 测试网钱包:用户需要一个支持测试网的 TON 钱包(如 TON Wallet),并通过 @testgiver_ton_bot 获取测试币。
- Node.js 环境:确保已安装 Node.js(建议使用最新 LTS 版本)。4 e; b/ S2 U6 _( t' e
, {1 z( Y1 O. @: K8 `5 r
项目文件夹结构/ C, H/ {8 R1 j. r* j; X
我使用 Vite 创建了一个 React + TypeScript 项目,项目名为 TON-RED-PACKET。以下是项目目录结构
5 ~4 H' a# y4 X# a# h( `5 S3 _* Y3 P; {$ P6 U1 L
7 _) r s) j9 e, s
7 E3 y3 \- @, a! r* D
目录说明:
# x7 @# n! l8 V" W- Z1 Jcomponents/:存放可复用的组件。4 e! ?5 ?- q% L1 C) ~& o
- WalletConnect.tsx:钱包连接组件。
- RedPacketContext.tsx:红包上下文,用于管理全局状态。, p' C* r l+ g2 Q$ r+ v
pages/:存放页面组件。
5 @9 m* N7 _% ~. w- ClaimPacketPage.tsx:领取红包页面。
- CreatePacketPage.tsx:创建红包页面。
- HomePage.tsx:首页。
- LoginPage.tsx:登录页面。
* E- k* A2 J' f3 W2 |3 h$ F App.tsx 和 App.css:主应用组件和样式。
( L: ~% ]7 F2 Y* T' ? iindex.css 和 main.tsx:全局样式和入口文件。9 m2 n% [ a' c4 n' s: t
vite-env.d.ts:Vite 环境声明文件。! g p$ A, a5 B; C$ x
其他配置文件:如 package.json、tsconfig.json 等。
0 A- a- L/ g4 M; g. O# i步骤 1:创建前端项目
6 |) Y/ |6 B+ b' ]2 a8 s我使用 Vite 快速搭建了一个 React + TypeScript 项目。以下是创建步骤:
' s8 c( f+ B# s7 A
$ y W8 Q1 M( P3 r+ j' w# m初始化项目: 在终端运行: g( r/ p, L! Z" Z" J( v" Y! A
- npm create vite@latest TON-RED-PACKET -- --template react-ts
复制代码 选择 react-ts 模板,项目会自动生成基础结构。; H, M1 i1 y" ?- H! N) B7 V( S9 B
+ B, r. I& R1 }0 O$ o! D* l$ {* G
进入项目目录并安装依赖:
! a5 w+ L; f9 a' a. @' t- cd TON-RED-PACKET% r% R( g1 B" g( s- A) e
- npm install
复制代码 安装额外的依赖: 为了实现钱包连接和页面交互,我安装了以下依赖:
5 L% h; u* H1 s) M ]: v- s e8 {3 {2 \7 d% w- f1 }
- npm install @ton/ton @tonconnect/ui-react antd-mobile react-router-dom
复制代码 打开浏览器,访问 http://localhost:5173,你会看到 Vite 的默认页面。
7 q) B0 K* A0 m6 s. U% X" k2 t8 j5 y
( z, N1 Y" p# P$ u
1 E) m# N* C0 _$ W. e. J& a! }, M! A B ^: c$ d
步骤 2:设置入口文件和全局样式
/ @# O# W# ^: h. w! `: ~我修改了 main.tsx 和 index.css,设置了应用的入口和全局样式。1 t# q, u6 U. u k
; N; [7 ~; W& F& C4 W2.1 main.tsx
" V0 e4 J5 K! P/ H d0 Wmain.tsx 是应用的入口文件,我在这里设置了 React Router 和全局上下文:( t0 \7 ^5 q' }% Q) R
- import { StrictMode } from 'react'5 B8 O7 U" {- L3 T
- import { createRoot } from 'react-dom/client'" S9 ?7 G; y/ J: ?
- import { BrowserRouter } from 'react-router-dom';
0 t3 `( i4 F8 T* K8 ` - import { RedPacketProvider } from './context/RedPacketContext';; W9 b, x5 F. z; r
- import './index.css'
; C2 R$ s' N! F( f" W: k - import App from './App.tsx'
* i( k3 z7 j" C. h
2 W* f2 ~3 e! |) V0 H$ c- createRoot(document.getElementById('root')!).render(# D; a" e( ?5 a9 ?/ ^' H
- <StrictMode>7 T1 W1 w" C. y- K/ N) t
- <RedPacketProvider>
/ @. A1 D; X& @, ~/ U/ j - <BrowserRouter>
1 P" i' l3 a1 r- C* m - <App />
% f8 b4 [5 W6 T6 D2 D' o/ d - </BrowserRouter>
3 u0 ~6 ?# _5 h. ?$ [# a' m" R - </RedPacketProvider>/ i% f2 p! i4 y: w1 w& |7 ~) B/ [
- </StrictMode>,# K/ A N5 s' p" f5 c" m: h
- )
复制代码- :root {" _) ^% Z$ Y( H& [( P( g
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
1 m8 q$ ~& U/ Y# E - line-height: 1.5;( k8 U8 m/ K8 N2 G
- font-weight: 400;$ ]9 Z/ E( `6 x2 r
- % q4 n- I! B# o& P& c; e
- color-scheme: light dark; X, M: e8 \$ u5 M* g* E
- color: rgba(255, 255, 255, 0.87);: q2 f- n9 k- ~2 e
- background-color: #242424;
+ c3 K( \( u/ v2 B- a - . }: L3 t1 I5 K# n' \) `3 v7 ?
- font-synthesis: none;% b6 N4 i. _# V. ]5 \9 d0 q
- text-rendering: optimizeLegibility;7 ~% x" Y7 U( q e/ ^
- -webkit-font-smoothing: antialiased;, a \' @7 ?4 m/ O$ S
- -moz-osx-font-smoothing: grayscale;
2 R* F7 {0 v4 A% N/ Y - }: n5 d3 U! W+ @
, z. P( e1 `0 P. V6 K5 s- a {
$ j9 m4 P) b7 Y2 a: U - font-weight: 500;
; p) L2 u' n4 E) J. f+ l' ]( o - color: #646cff;
p- d) E$ y! K5 L/ _" U+ _ - text-decoration: inherit;- m! J4 K5 M2 }' ]& f# q, _" ]
- }8 B, @3 b3 r. b! Y8 v
- a:hover {
0 S' D! ~. S& w8 A$ c+ C( B" X - color: #535bf2;
& L4 |4 i6 |0 L, { - }+ Y, G/ d& O& t
- & V: h/ E. m8 ^/ ?4 d* h Q/ D
- body {
( u3 H* L9 u( Y - margin: 0;$ g# C2 {( i& x& y; ^9 k$ J
- display: flex;, [' {+ l" S: E8 k2 B7 D4 e2 V8 C
- place-items: center;5 D- k) |% U" x6 f
- min-width: 320px;
! s4 f O" u/ E% }9 @5 u8 E, f( O - min-height: 100vh;) E1 Q# z. d( r
- }
9 J' G+ b2 L6 v( U - : ? g: I) t) \7 h6 [: O& E
- h1 {2 P( @; A" m$ ^" `
- font-size: 3.2em;9 q9 W& e! t4 V
- line-height: 1.1;
! m* G% P4 B. A& U% w - }
# [1 R' o: G7 { L% {7 F
7 Y( { e' n) s* {, ], Y3 z- button {1 z! ?; M7 B4 [/ @
- border-radius: 8px;: q S# B9 f4 Z+ Z% |* U
- border: 1px solid transparent;$ b; a/ w/ l$ C. t6 Y- u
- padding: 0.6em 1.2em;
& ` b! P" ~6 D+ [/ W4 [/ g7 u- g - font-size: 1em;1 V, c, v2 q% I' ~
- font-weight: 500;; t0 A6 [0 Z% F, ~/ s3 d/ p; }
- font-family: inherit;- v! T! p4 Y7 {9 p
- background-color: #1a1a1a;
& U) F$ o2 ]* M7 G0 n) T - cursor: pointer;
% l: O! `9 p5 s0 \" p - transition: border-color 0.25s;
/ e' j' [$ Y/ w - }2 u1 F# I4 d1 j K. M
- button:hover {' F) N! v& f+ K) f9 d7 w3 X
- border-color: #646cff;
/ ^* ~: O8 v8 V% }% U - }
3 g: _/ h" _" w7 K, ?/ L; x2 @ - button:focus,
. O1 T0 \' i4 [6 S% g& h - button:focus-visible {
r/ X2 ?) L0 y, s* h- `/ U! x7 I1 ?. p - outline: 4px auto -webkit-focus-ring-color;5 @! S$ I$ ]& n" [8 ~/ _8 {; Q
- }
, k5 y3 ~% Q% O2 I
: \7 c' l2 `* E0 K9 P$ P" ^- @media (prefers-color-scheme: light) {
2 S, D! _7 \& x7 v# m; f3 K2 B# x7 g% w - :root {! |1 V ~. a" a
- color: #213547;9 ^1 V, T. ]; t! j4 k
- background-color: #ffffff;
- g6 g7 d& \& ? - }/ H$ b2 @7 j' b2 c8 ?6 a
- a:hover {; A( h B& X# n4 j. s
- color: #747bff;
7 I5 d1 {: o1 F; e: Y) K) D' B - }
7 @) U1 D1 K* @0 T! i7 Y8 F9 E - button {0 }; y. x! J% P8 e+ j
- background-color: #f9f9f9;
) u2 D& Z- j: L8 } - }9 k: K. t) w! g. R0 y3 R% X; W
- }
复制代码- // WalletConnect.tsx
+ e2 D8 y$ Q& t - import React, { useEffect } from 'react';
. G/ o, `* u0 \# e) k7 B - import { Button } from 'antd-mobile';
# V7 q+ E* X A4 V9 |3 j G - import { useNavigate } from 'react-router-dom';
2 a- B; w7 G3 Y o, B - import { useTonAddress,TonConnectButton, useTonConnectUI } from '@tonconnect/ui-react'; // Import necessary hooks
4 Q! o0 Q3 n5 l8 O( C. V: }( v
% I. ~+ ^; _& ~) s( {( k7 m- const WalletConnect: React.FC = () => {7 {6 K z5 r0 n
- const navigate = useNavigate();$ U7 j) }; j( a o3 T
- const address = useTonAddress(); // Get the connected wallet address
. Z& a! }* {- U' G! T% V - const [tonConnectUI] = useTonConnectUI();
/ S7 B$ j, ^3 \' e - 9 ?' J- z9 u/ O% ?
- // Redirect to HomePage once connected
6 o9 Y& M# p/ f- n: @ - useEffect(() => {2 |% L k, \. |
- if (!address) {
9 i( x$ A( r, K- P. ^0 G - navigate('/login'); // Navigate to HomePage when wallet is connected
4 Q. B% E& b0 k' T7 k6 J% i - }
6 A% V7 |: b+ ? - }, [address, navigate]);
; P1 B2 K; D! O4 P O" g0 E - 3 h% q: o4 B) R: ~, o' {# L* z
- // Handle Disconnect Wallet5 Y& Q7 r5 l1 ^1 j. W! s) t
- const handleDisconnect = () => {7 g# a( B4 O6 T& e8 s, R' C' {
- tonConnectUI.disconnect();: l: u: L; g4 _3 U* B
- navigate('/login'); // Redirect to login page after disconnecting
: V& u( c. n: V0 \ - };7 K% M w6 k! ~ R" X% x) x
- ( F8 D* G& d9 }+ Q7 J
- return (
+ k" C y9 k8 r: _9 D - <div style={{ padding: '1rem', textAlign: 'center' }}>+ B! T! f$ v& F( m
- {address ? (
8 \. F$ |/ {0 p3 Q* @8 |2 x; x - <>9 F: r. ~5 y9 `9 r
- <Button onClick={handleDisconnect}>Disconnect Wallet</Button> m1 B( R% ]! R g8 I, T8 O
- </>
/ S, ?9 }, S5 O' R3 B* `5 S( r$ G - ) : (
- V3 ?" R" o1 F6 N8 y; O z - <>. p; w( d! u! m8 V! E1 t+ }) k, O
- <TonConnectButton style={{ marginBottom: '1rem' }} />8 W( u6 [# K9 `# B, W
- </>
! b f$ {/ p" P1 j5 G( y% s3 N - )}
( J: o; \$ @1 _% v. L! f2 A - </div>/ M7 g! E2 j( h, K
- );
% n3 _" }( P( m' x" U - };
5 p" K7 E1 [7 l! _
4 |. O Y; ?4 R" [! T- export default WalletConnect;
复制代码- import React, { createContext, useContext, useState } from 'react';% u5 \! N9 [. j. F( y
- z+ k; V+ s% @ B5 q
- export type RedPacket = {
" O% K6 s) D( L/ l* {# C - id: string;/ r' w1 p; L9 l2 q+ m
- amount: number;7 E4 T7 i4 c$ _2 d# `) o4 y# R5 P
- quantity: number;" N* G; V u" o/ U
- message?: string;
4 F* t, \: `/ @2 u - };
" l9 t1 H) F* `+ h/ ~
, y# u& l7 M4 P+ ^. S; q- type RedPacketContextType = {
2 Q6 q) a# B9 P1 p* c- m - redPackets: RedPacket[];
W9 `- A8 R6 _; \/ @* G" C - addRedPacket: (packet: RedPacket) => void;6 l+ \5 F1 }; U; x
- };+ @: `, b. p5 a% z. R! f7 t2 a k; f
! X; h# o" E: E. }- const RedPacketContext = createContext<RedPacketContextType | undefined>(undefined);6 j; h+ O. G, O
! N+ Y- a: Z+ D% ~- export const useRedPacket = () => {3 x; D* A% Z: f5 H8 f$ a
- const context = useContext(RedPacketContext);
8 U9 d: k( W" m: W$ z {) g( X$ I - if (!context) {' c! c# U4 e+ t% ]
- throw new Error('useRedPacket must be used within a RedPacketProvider');
' x6 t: B/ d# C n8 m- d - }
' p+ M8 Y7 @4 i: F - return context;; k( X/ C; q+ @8 o2 l# F, H) R& X$ Q
- };6 F' E, B X x% `: }
- ) c3 d0 c$ O( e" }5 A# I t
- export const RedPacketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
7 u8 U+ X+ `* n& N - const [redPackets, setRedPackets] = useState<RedPacket[]>([/ |1 S6 }: A; L: {- `3 i( ~
- // Initial dummy data; B4 \, s+ u5 p8 X( Q
- // { id: '1', amount: 100, quantity: 10, message: 'Happy New Year!' },
; y# V* X. W+ p! [1 H- k - // { id: '2', amount: 50, quantity: 5, message: 'Best Wishes!' },
* i/ {4 K! Q. y4 |2 Q - ]);
u a9 o% T4 V/ X1 r - . i7 f/ N, K# e# c7 A
- const addRedPacket = (packet: RedPacket) => {
4 W( z6 @7 V2 @6 B - setRedPackets(prev => [...prev, packet]);4 W9 Z" H1 B8 y3 o
- };) G1 l( T- r( k8 c. c& c
- ) U3 ]& j$ i! n& h d9 N
- return (
2 G) W/ f. o! c. P - <RedPacketContext.Provider value={{ redPackets, addRedPacket }}>/ T# N8 P8 {# {& k0 ]( P
- {children}1 @: F7 `% q, X @3 _
- </RedPacketContext.Provider>
0 C, \& }/ [# f' J - );: E* H' E" G) {
- };
复制代码- import React from 'react';5 q; ]% d. r3 V) ]( h7 N1 Y
- import { Button, List } from 'antd-mobile';
0 p- G& a5 k! v4 T6 u - import { useNavigate } from 'react-router-dom';( _& }+ V4 Y: m8 U
- import { useRedPacket } from '../context/RedPacketContext';
' h, p9 `- ?2 }, T( S
9 `. x* S5 n2 [; g% m5 k+ U% e- const HomePage: React.FC = () => {
" i/ |. t& K P - const navigate = useNavigate();- e5 Q2 O |. ?4 v
- const { redPackets } = useRedPacket();& O X% z4 N' p7 n
7 G$ w1 I# g1 ^$ _* Y( n- return (
4 W5 M" t& D7 j2 \9 F4 I) n- M6 K' P - <div style={{ padding: '1rem' }}>' }0 L( N. S# E/ G5 y
- <h2>Home</h2>
) t- b' D# ?7 C - <Button color="primary" onClick={() => navigate('/create')} block>
# e- l* {+ f" X1 J" a1 y7 {- ~2 h - Create Red Packet4 F6 y0 i* i6 Z9 i" @
- </Button>
# d6 ^0 T/ k- v( ` - 5 d1 }" `1 X1 N& ~1 t" X
- <h3 style={{ marginTop: '1.5rem' }}>Available Red Packets</h3>
7 W; e6 w1 J5 d+ @ - {redPackets.length === 0 ? (, g2 o3 l, e! K8 s
- <div>No Red Packets Available</div>$ }$ I4 J* b$ f& l) D
- ) : (
% r5 ?; ?$ t$ g - <List>
0 Y& B8 K. U9 J- B - {redPackets.map((packet) => (. ~, J$ r( _# U! f z) h4 f) v
- <div key={packet.id} className="packet-card">3 S3 ]8 g S& r m$ ?# O
- <div>
; R$ k; g; g( ?* S/ g4 }$ ` - <strong>Amount:</strong> {packet.amount}# l; q. f: l, k0 X* Y; V. y
- </div>/ I, Y( m5 s% G2 {) ^8 J6 k/ _* B' M
- <div>( R. t: _5 P& J$ U( W1 V+ C+ I
- <strong>Quantity:</strong> {packet.quantity}
+ I7 f; w% a1 |# @3 {" S6 H - </div>: N1 B# t! p9 D! K1 B# u+ x
- <div>
3 K2 J3 Z; }& T- N+ D% X, h8 k - <strong>Message:</strong> {packet.message}, f" j+ ^# @/ \' }
- </div>
7 K7 ?4 M T7 q1 \6 B2 m3 K - <Button" _: X4 m5 V: v6 _: J: z1 b; d- n
- size="small"
+ U3 v( I6 ?) c( O* _, a - color="primary"' |0 x1 l T$ p3 N" E! {9 b
- onClick={() => navigate(`/claim/${packet.id}`)}
1 y- E9 v5 r7 N1 f+ M - >
" N/ K* h4 ^' k# t4 h! ~* ` - Claim/ I4 h/ ~5 m8 ~+ t* {% j
- </Button>5 o0 ~, i0 a4 o/ ^
- </div>
7 q) e, r( s2 r. }' g* ~ - ))}4 t2 m$ D- e7 z8 C ?5 m" C$ W
- </List>. X, k; ] O3 C+ Y
- )}
- ?' K. I; J: R! n* J3 B$ o - </div>
9 [- L2 n. [2 J3 N - );% {- u% [. X' k% m
- };
$ ?# W+ V4 f/ g1 m. z" F+ b/ i, E - # N3 V$ c# N1 x
- export default HomePage;
复制代码- import React, { useEffect } from 'react';! T# i; g2 f" J6 }% n6 K
- import { TonConnectButton } from '@tonconnect/ui-react';
Z3 y3 z9 S8 e( \1 A - import { Button } from 'antd-mobile';" l5 k6 o& S" ^0 B' L) h; m% m
- import { useNavigate } from 'react-router-dom';4 V3 V/ z4 i0 h3 g/ ?) q( ]
- import { useTonConnectModal, useTonAddress } from '@tonconnect/ui-react'; & X# _8 i0 {- P9 k2 H1 Y
- " P$ j7 J% {# S0 K. m, l) D
- const LoginPage: React.FC = () => {: B( y: x8 [$ v+ U. {7 V' v8 d
- const navigate = useNavigate();* P, Y. l4 c0 P! ^% i `* T# @% ^1 V
- const { close } = useTonConnectModal();
3 F: }, y1 _/ X; w$ x6 { - const address = useTonAddress(); : |" Y+ ?3 J. E3 S0 W, k% O3 _
* M$ l. I. {* f, Q8 v- Z& {- // Redirect to HomePage once connected
3 X2 l; T( \8 y8 z- w - useEffect(() => {3 A) Z# m# J L' ^* v; f5 W2 |, M
- if (address) {
7 |+ E) e4 W6 a2 T& |+ B - navigate('/'); // Navigate to HomePage when wallet is connected( D4 q7 i2 v: \& C; t
- }
2 I# Q5 u7 }, N8 F - }, [address, navigate]);& R' @4 v- ?* Y' }. L _4 `
2 @, _; j0 U5 y% R: e+ e% [- return (
& w! y# w3 R2 G X2 u( a - <div style={{ padding: '1rem', textAlign: 'center' }}>5 e: l+ W0 T( U j' @' u
- <h2>Login to TON Wallet</h2>; x* w7 y- _. R/ C3 w6 a
8 [9 s, _& J0 c5 e* l& c) W4 W- {/* Show only TonConnectButton for connecting */}
) `9 G8 h1 d) k" N+ s - <TonConnectButton style={{ marginBottom: '1rem' }} />
: \4 ?* Q0 g$ v - 5 G* z4 n5 y+ [5 w8 Z
- {/* Only show disconnect wallet button once connected */}
5 |3 m$ v/ Y4 N7 F/ H* u, M - {address && (( f! S X$ V. Z0 X
- <>. p% H) `' P3 p2 p; C4 e
- <p>Connected Wallet: {address}</p>0 g# M) u* B% B- Y
- <Button onClick={() => close()}>Disconnect Wallet</Button>
7 F( P; O6 i; q2 ^1 {' m T9 [" b - </>5 F x7 f( D# n+ h
- )}
0 C& a5 M* T. r2 x - + |! ?( y; k+ E- S3 p
- </div>
5 {. Q* l I( B1 K, G - );
, V7 A8 t8 q! b0 C# r' |3 Y# u4 `, c - };% [/ S+ d" x) d% V K4 b
- 8 O, ?1 H/ E: W( o
- export default LoginPage;
复制代码 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|