|
本帖最后由 riyad 于 2025-3-20 21:47 编辑 8 ^) s! U9 H" O+ Q! q
# {- `, {* I$ E' w2 W t在前两篇教程中,我已经完成了红包智能合约的编写、编译和部署到 TON 测试网。今天,我们进入第三部分:开发一个前端界面,让用户可以直观地与红包合约交互。我使用 Vite + React + TypeScript 搭建了前端项目,并集成了 TON 钱包连接功能。以下是我的开发过程和完整代码,欢迎大家参考!$ {+ u* j0 B) Z: s5 D$ {
* n5 F1 T1 K3 |* k# h前提条件
* T; B7 z4 G/ ^2 E) y在开始之前,确保你已完成以下步骤:/ H) F& t r Z. r! [
, X1 Y( R0 E% J9 [ A. h0 _* }0 X
- 完成前两部分:你已经编写、编译并部署了红包智能合约到 TON 测试网。
- 测试网钱包:用户需要一个支持测试网的 TON 钱包(如 TON Wallet),并通过 @testgiver_ton_bot 获取测试币。
- Node.js 环境:确保已安装 Node.js(建议使用最新 LTS 版本)。
1 G# d& S/ e4 F) D0 S / Z, N3 F7 T1 [6 L7 B8 E
项目文件夹结构4 r+ \3 v. S! l W! l/ H
我使用 Vite 创建了一个 React + TypeScript 项目,项目名为 TON-RED-PACKET。以下是项目目录结构" T" c& K& _- v! d- W/ _
/ T2 a2 k, N/ s
2 Q2 F |" \# Z" c) y
, U4 q; \ p' r2 l
目录说明:
1 K0 w; N- v J4 m4 [$ ?components/:存放可复用的组件。
# ~$ Y. P9 K2 G7 x; j# n/ d6 [- WalletConnect.tsx:钱包连接组件。
- RedPacketContext.tsx:红包上下文,用于管理全局状态。+ g) z' D. M; J6 L
pages/:存放页面组件。
) X* k' V0 J7 O5 {- ClaimPacketPage.tsx:领取红包页面。
- CreatePacketPage.tsx:创建红包页面。
- HomePage.tsx:首页。
- LoginPage.tsx:登录页面。1 V0 b3 F# n5 d$ ~& `
App.tsx 和 App.css:主应用组件和样式。
4 C: ]9 O1 t( h [6 I6 oindex.css 和 main.tsx:全局样式和入口文件。
% K3 d1 e# r( I# q4 evite-env.d.ts:Vite 环境声明文件。
5 a- i+ `1 |9 x% u+ U其他配置文件:如 package.json、tsconfig.json 等。
8 p* Q5 o3 [; ?) ~& v* C5 e3 S步骤 1:创建前端项目* o% ~$ J# Q# c! B) j
我使用 Vite 快速搭建了一个 React + TypeScript 项目。以下是创建步骤:
: s, a2 N0 h/ o. K0 u( E$ {
y* t9 {( n/ M& U初始化项目: 在终端运行:
5 Y$ P, B. ^# A. J- npm create vite@latest TON-RED-PACKET -- --template react-ts
复制代码 选择 react-ts 模板,项目会自动生成基础结构。5 `6 M4 U9 P5 G" g# u4 h) C% i
/ X9 v, f6 w) n' J# o# N
进入项目目录并安装依赖:3 R7 r7 Y* a [2 P5 Z8 ?
- cd TON-RED-PACKET
$ e. C( c, z" J2 X3 L - npm install
复制代码 安装额外的依赖: 为了实现钱包连接和页面交互,我安装了以下依赖:
+ E5 _4 b" q6 F( L& c$ p1 J; b3 J) @- b' N, q: d$ _$ A
- npm install @ton/ton @tonconnect/ui-react antd-mobile react-router-dom
复制代码 打开浏览器,访问 http://localhost:5173,你会看到 Vite 的默认页面。
, S# d2 Y8 |$ X m6 q* S' X& v3 B, ~5 p2 \7 \ G, W
{3 I* v9 L, J/ y8 X/ H, U( B" f5 m8 S9 Q
步骤 2:设置入口文件和全局样式
5 `, B. Z6 D8 o- x7 K! p% h/ c. ^我修改了 main.tsx 和 index.css,设置了应用的入口和全局样式。, C) l1 Y# n1 |4 W4 [
- G* \* j6 G* M2 Z2.1 main.tsx
" I) `& Y$ g( n+ a( t: [main.tsx 是应用的入口文件,我在这里设置了 React Router 和全局上下文:
* C$ j$ j/ }- F5 @! I# x k7 e+ N* J- import { StrictMode } from 'react'8 a* P5 [+ s# f: _
- import { createRoot } from 'react-dom/client'
. E0 ^+ D$ r6 y1 U( f" i - import { BrowserRouter } from 'react-router-dom';; G4 p. A# e) f* Q/ ~
- import { RedPacketProvider } from './context/RedPacketContext';
# b6 B& l# {. B8 w# _- U - import './index.css'
0 T2 _8 w6 ~3 \+ T- o5 f" z" z - import App from './App.tsx'
) E6 D" H5 q3 C+ A" ?% }
- w8 c& t0 Z2 Q: n# V- createRoot(document.getElementById('root')!).render(( m2 b9 g+ F7 T' M! u
- <StrictMode>
* U. }6 O/ j; ?# f; K) d - <RedPacketProvider># R( Y$ B V8 C! H2 E6 f$ g
- <BrowserRouter>
8 x. l4 V. B+ L7 W7 G, v - <App />, w0 h9 v; t1 `+ h* Y; o
- </BrowserRouter>: K- ?' v: w' n# |" K6 l
- </RedPacketProvider>& j! z+ B4 z4 M5 M& h! n& _& G8 h
- </StrictMode>,( r) q) n7 q/ j8 f. {: w2 ]5 u' M6 w
- )
复制代码- :root {3 g4 v" ]: X |4 X [# D
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- X) Q: [' {3 N - line-height: 1.5;2 n- q$ | E/ k6 a0 h0 u! ?
- font-weight: 400;
% G' t$ ~: d e8 l2 a9 L - $ [: O* t& H7 p; O
- color-scheme: light dark;
$ v7 j3 j" D4 R - color: rgba(255, 255, 255, 0.87);
( ~3 u R: o2 u/ ?7 A' p - background-color: #242424;+ ?7 ^+ L$ y7 h$ U
- " M+ v$ R8 |# S+ L
- font-synthesis: none;
8 p$ N2 k' W& B3 z% y4 b - text-rendering: optimizeLegibility;, r! D. _% T+ H% L
- -webkit-font-smoothing: antialiased;
$ ^3 d* i6 z: A5 x - -moz-osx-font-smoothing: grayscale;, ?% q% V6 V. i' R) \* d
- }. a% U. n- ?# ]7 u3 `. M- v
- 4 D+ n# C d8 w/ m" q2 z0 ]
- a {0 @7 V }! n7 O1 w6 T- I! s. ^, b4 _
- font-weight: 500;
% n4 ~. R+ v8 p; a - color: #646cff;
2 X* N, S, T( f5 D1 M8 y - text-decoration: inherit;
, b+ g) r0 c( k7 T7 Z, m1 @ - }
' J6 L! W# k }2 G4 A! h - a:hover {* y4 W3 ?' ?; H9 G* e
- color: #535bf2;: O& k* _" O) p- f- e0 c6 J
- }
* k u5 |4 r+ t% Q, _
/ f; Y4 M) Q k$ p8 B* L- body {2 |( q- f$ Z: H+ Y) k/ o6 ^
- margin: 0;' a. Y9 N7 t, h/ E
- display: flex;5 O+ A0 p) G; O1 z( L
- place-items: center;2 P) F0 J: v2 s/ L( d
- min-width: 320px;
7 n: Z) U' ?# i- ?3 W! | - min-height: 100vh;1 i4 P% d3 p8 ?$ K6 z
- }' W" `8 s) M5 O* K5 C) o
4 Q+ P- k ^5 O; j- h1 {! Q$ n% i) b+ p* |. w( S
- font-size: 3.2em; j4 O; _+ H# p5 N4 ~, l
- line-height: 1.1;& z) ^3 J8 s5 B( G
- }5 @4 w8 c+ T2 a! m3 t
- 8 K3 D4 Y: k2 R( a' x" |
- button {# x( @/ l' c; p4 z- z0 g! C0 I
- border-radius: 8px;1 z3 w# x/ n, O
- border: 1px solid transparent;
0 v4 E3 Z1 M5 |8 G. d. J* {3 l - padding: 0.6em 1.2em;2 m9 x7 f# n9 x7 X6 W' z( R0 u
- font-size: 1em;+ j) @ w+ Q' x
- font-weight: 500;3 ^4 G( O/ n' _: M3 b5 ]6 T8 u! I& M
- font-family: inherit;
c9 w, \" Z; ~7 O - background-color: #1a1a1a;1 @* A& i9 k" e) Q% J4 T' @- j
- cursor: pointer;
! W) k+ _0 k7 f0 b: _; V5 ~% [. V - transition: border-color 0.25s;' O! S" j o: L+ a& y A
- }0 E' m! L* r: _
- button:hover {
: T- u2 A) M7 T( x: ` - border-color: #646cff;
) C. s2 b# n$ }* Y. P - }
8 n8 N7 C9 R& G$ t8 ~ - button:focus,) W- K5 x7 T4 O$ y
- button:focus-visible {
0 w+ l$ h& r! Q7 ] - outline: 4px auto -webkit-focus-ring-color;* p) Z2 K. B7 v3 e: a9 N! k
- }" N5 f: s2 P6 p# _3 u! \
# D* G- \& q4 g3 y( v6 G) r- @media (prefers-color-scheme: light) { m" X9 h; M+ a* S- v3 D( {5 L4 d; `& k
- :root {
2 J* e" P2 C C7 X3 X3 ~3 f - color: #213547;
: O: q" B4 s! D" {( k5 z - background-color: #ffffff;0 N3 i- i7 G* d! Z+ {5 p: [
- }
% C% Q& {. B- Z8 a - a:hover {
; Z6 `+ Y7 b, {* j: \5 q9 t& P# ` - color: #747bff;
0 _" Z+ M/ a& E6 _" o4 `! G, t - }
! q% q# r* \" I% l8 y7 _ - button {
* N+ v# w5 G5 x. [6 { - background-color: #f9f9f9;- ]" v. h o. T* G* _+ |
- }
8 o O, v+ H( f - }
复制代码- // WalletConnect.tsx% E0 j( ~& F+ o9 O1 q4 K0 v
- import React, { useEffect } from 'react';9 V5 ^" Z, C8 c4 y; b. |- W
- import { Button } from 'antd-mobile';9 ]8 H- L, Q. f9 l
- import { useNavigate } from 'react-router-dom';5 Z& F# N0 O o. {9 t. ?* I
- import { useTonAddress,TonConnectButton, useTonConnectUI } from '@tonconnect/ui-react'; // Import necessary hooks
1 `- E4 B( Z: O5 r9 g+ s
0 W: J& _- t1 t7 x; `, M9 ?8 I- const WalletConnect: React.FC = () => {9 g8 _5 R* r I5 e
- const navigate = useNavigate();
% l/ ?9 R& k u# H - const address = useTonAddress(); // Get the connected wallet address( {0 W9 _6 H% R3 j, `& E5 @
- const [tonConnectUI] = useTonConnectUI();0 i9 r( g/ ~& ~/ N( J# B2 Y& S
- 6 L% e5 o1 H( q- J& n
- // Redirect to HomePage once connected4 v6 B9 d+ D* f7 R$ ?
- useEffect(() => {
- C# L3 @! Y; H# l6 B - if (!address) {
5 P( z) w' b# v3 z - navigate('/login'); // Navigate to HomePage when wallet is connected9 z/ O6 E% |) u. k! R( l
- }
, p; A! B# T+ d- w9 o1 { - }, [address, navigate]);3 N3 F' _# G1 e# q3 @( P( Y; A
; O- \ { Q8 }) e' O0 Y- // Handle Disconnect Wallet9 ^3 K) c! L( p! U
- const handleDisconnect = () => {
# O) n# A1 J- s - tonConnectUI.disconnect();
. P- Y) K, X* |- M% O7 n, v! K3 _# e - navigate('/login'); // Redirect to login page after disconnecting2 v8 E$ K. T7 p5 k# t
- };
( d7 s0 m8 X; `! C% c" e& e
) B% ]& U$ ~+ L( s. F [( W- return (9 z$ S2 N; W$ Z5 f4 T! P* n6 ? H0 v
- <div style={{ padding: '1rem', textAlign: 'center' }}>
( V+ ]8 w! J" Q - {address ? (
; o5 T2 @" k: T0 E n; B+ m" [ - <>" x, q* W% ~/ A' `' ~' G4 Z
- <Button onClick={handleDisconnect}>Disconnect Wallet</Button>
3 e) R7 x5 h4 d& e+ S - </>
7 T) l7 P7 w3 H: X; u& Y( D; i - ) : (
& r; e" C+ ~6 l& E% n' s - <>& J! a) u' c# U3 k
- <TonConnectButton style={{ marginBottom: '1rem' }} />6 V6 Y. x. B* ~$ C3 J4 e1 p
- </>2 k* A( J) y w3 a) Z
- )}
% a8 |; C0 ]/ o/ H, B2 {. o5 v) w5 u - </div>
M) P" l& g+ Z5 y1 z& ? - );
' G# a' f2 U( @( ~5 x - };
9 G$ \) w* t' r. D
9 Y6 A5 Z# K/ D5 ]& ?9 ] o- k6 j' C- export default WalletConnect;
复制代码- import React, { createContext, useContext, useState } from 'react';4 w4 a2 i0 V* v w
! q( T' d6 ^; L- export type RedPacket = {
: L5 B3 s2 a; L1 k% I; g( e - id: string;
' Y9 [7 t4 w, b s9 B& L - amount: number;
2 M4 U: L6 ]4 g5 C( T( p$ _ - quantity: number;
. J" }# B& _8 n& y7 b, E. n - message?: string;
& b- i6 a5 w' u4 z0 Q - };
; E1 B7 `1 d6 p; v% @- W% S3 i
) X* i9 R9 b$ Q, W- type RedPacketContextType = {
* y% n9 K6 { ~' Q - redPackets: RedPacket[];
9 X0 I% b. f1 Z7 w2 h' ^" n3 { - addRedPacket: (packet: RedPacket) => void;
, I B0 l& X- Q5 W) k- z) e, A - };) {6 ~/ T% F3 h0 S/ @
0 ?8 K& _# e+ n$ U! E7 |; ]1 x5 @- const RedPacketContext = createContext<RedPacketContextType | undefined>(undefined);
6 I3 D6 y, A$ X+ D: g$ U - + o- Y$ ~* `7 }; Z( }2 `3 Z7 A
- export const useRedPacket = () => {% Q/ D5 p |# q; s
- const context = useContext(RedPacketContext);; U; j! f/ n+ ]% U" n
- if (!context) {
( Z g7 ]/ v- S) p - throw new Error('useRedPacket must be used within a RedPacketProvider');
9 [7 e E( t1 m! }: z" G2 e - }
& v7 Q3 ?8 S8 O1 v4 a - return context;4 @& |( Q$ j T# ?
- }; Z; P2 i& i# @% T" _" _6 O
- W+ X' R/ j* K2 @* T
- export const RedPacketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
" ?$ X) u: X( M - const [redPackets, setRedPackets] = useState<RedPacket[]>([
- j _0 Z3 w% o0 A- y; F/ d - // Initial dummy data
& L% q6 V$ ~9 R( G/ v h5 _ - // { id: '1', amount: 100, quantity: 10, message: 'Happy New Year!' },
+ q, l# P0 V, A - // { id: '2', amount: 50, quantity: 5, message: 'Best Wishes!' },! Y3 Y1 m$ a2 m! z5 [6 L d( b
- ]);
$ D# q9 V5 V6 n. {0 Q' m - P" K* a8 L8 R- \$ p- N a; [
- const addRedPacket = (packet: RedPacket) => {& P8 Z S" W4 q( i x+ C9 c7 r* s# P
- setRedPackets(prev => [...prev, packet]);
8 K) e6 N0 ~: J2 F - };1 H. d, K: g4 M. h$ N) N
- $ \9 S2 x2 B7 }2 b) j, N
- return (
6 F3 m' {6 e, K0 S ]& y c F - <RedPacketContext.Provider value={{ redPackets, addRedPacket }}>! k, A& b! o2 _! H4 C P* @
- {children} c+ c% y) O S! p% ^
- </RedPacketContext.Provider>
y1 s0 S9 N& v7 V6 k5 k - );3 K" J& Z- k/ t- t
- };
复制代码- import React from 'react';
( A4 K: ^0 m5 B" q0 R, L - import { Button, List } from 'antd-mobile';' D# `7 e& A$ j4 q# l1 R
- import { useNavigate } from 'react-router-dom';% `! A3 ~& E5 R5 l$ w. Q
- import { useRedPacket } from '../context/RedPacketContext';
, ^+ n% L% e n7 B( ? - " ^; q) W9 r- X0 z) a3 `& ]
- const HomePage: React.FC = () => {
- W0 \& F. m9 d/ ^9 {) C - const navigate = useNavigate();+ A& i' o3 z( ^1 l( l, \
- const { redPackets } = useRedPacket();9 E- h9 e& Q. k; P/ b
3 D3 W* q2 V. V* d8 Q* n4 t- return (
Q% j0 o3 \6 H& F% i/ e - <div style={{ padding: '1rem' }}>
9 q7 B: d0 f/ ~$ L) Q - <h2>Home</h2>5 G0 Y( S) W+ G9 U& s8 B- W6 a6 O! P
- <Button color="primary" onClick={() => navigate('/create')} block>
- x/ s0 ~7 x: a* Z - Create Red Packet; T# m7 d; X8 W4 k9 l' i
- </Button>3 H( e- Q0 T, q" J' F# O p
- % _& ]9 g( l, g S
- <h3 style={{ marginTop: '1.5rem' }}>Available Red Packets</h3>
+ i1 {7 k8 [" }7 r - {redPackets.length === 0 ? (
+ N. K+ r L; \& [! L, @) K/ c - <div>No Red Packets Available</div>6 n/ J: s/ v+ b, a8 |
- ) : ( w& v/ r6 Q; T
- <List>
( l/ e; ~ T. S% O4 k" O - {redPackets.map((packet) => (2 o. q( C+ U9 r4 p- ?7 W3 S* ?
- <div key={packet.id} className="packet-card">/ [; q; v( r' J% S/ i% I
- <div>
& f* F2 p& |1 P - <strong>Amount:</strong> {packet.amount}
0 U' C2 [: r x+ B$ ^; d - </div>
8 E' t* W9 @% U, Q8 J - <div>
5 @% ~; @8 y H - <strong>Quantity:</strong> {packet.quantity}
; t5 }2 B q" J( h' f - </div>
7 Y ]! i5 G9 D/ n+ Z - <div># ]/ F# Z, a. E9 k' m3 I+ |1 I9 I$ H2 G
- <strong>Message:</strong> {packet.message}3 r! X/ g$ [, i# x
- </div>' a5 p( @8 X' ?+ l/ q, M
- <Button
- i: i9 ]* q5 U: k - size="small"
& ~ e& A) A, w - color="primary"
$ N0 t+ _3 X% L4 ~ - onClick={() => navigate(`/claim/${packet.id}`)}
4 `, F# m8 L8 e. A5 s - >9 c2 h i) d! N; |
- Claim0 F) y$ x$ Z. }
- </Button>5 }/ H$ i# y: r7 ^* J2 U
- </div>
8 B, ?5 J6 T/ o - ))}
, _: U2 c3 c8 U( i8 P1 R8 s+ W - </List>
1 L! D; E4 n1 E5 l( m$ J4 K: {, ^ - )}$ t/ `& r4 f. R& M
- </div>
. b- [; ~4 w1 d8 v1 s - );
1 k1 o; W- {0 w' [1 ]. Y& K - };+ ~1 T6 k% E7 N: ^& y* ^
- ; P4 b% [) N! l s5 H' |7 o' h
- export default HomePage;
复制代码- import React, { useEffect } from 'react';
8 n" y+ a' J/ A! b) Y - import { TonConnectButton } from '@tonconnect/ui-react';
' N: ?% \% I: b2 `0 L5 K6 Q/ @! \4 ~ - import { Button } from 'antd-mobile';
: E5 t" D$ Z7 k( }: \ - import { useNavigate } from 'react-router-dom';
. n# v1 G& ?3 n# c# i$ ~ - import { useTonConnectModal, useTonAddress } from '@tonconnect/ui-react'; 9 t c! M" F9 D: s. `" h/ Z
- 0 J" E9 P0 g8 K: ]: G4 a
- const LoginPage: React.FC = () => {
/ @+ J$ u; T' K0 s& B - const navigate = useNavigate();
* ?; [! ?$ I1 Z- P3 N" r - const { close } = useTonConnectModal(); 0 ]4 V3 K6 X* A2 `) p9 w
- const address = useTonAddress();
( L: _$ U5 O0 N5 O$ U* ?
# p O, z. a+ X t7 Y- // Redirect to HomePage once connected
4 N" v6 v6 M# i2 S - useEffect(() => {' G# c$ }0 A, B7 y. [" W
- if (address) {7 ?9 K* V+ {8 c; N( A G4 l8 O/ o- d
- navigate('/'); // Navigate to HomePage when wallet is connected q: o$ q: E% u2 M4 A* H0 C9 c' i
- }) u9 ^$ _ p2 }3 d5 S; \. A$ Z
- }, [address, navigate]);$ j: K; Q3 \5 a7 v
. f4 ]3 T( l8 B# M, y- \- return (
* U7 R: A! b& T - <div style={{ padding: '1rem', textAlign: 'center' }}>/ {( y5 M/ a4 c! J
- <h2>Login to TON Wallet</h2>
! [; h( `7 t3 d( ? - ; z, M0 f' ]. A& K
- {/* Show only TonConnectButton for connecting */}
5 @1 B& i' r, \3 R4 y9 g - <TonConnectButton style={{ marginBottom: '1rem' }} />% R1 d5 I. o6 I
- ' S( ?+ h8 O9 `4 r9 O
- {/* Only show disconnect wallet button once connected */}
* q1 y" K; @8 B# A - {address && (: e5 \7 f5 B1 g
- <> W" @; T( M- b$ u& G4 G
- <p>Connected Wallet: {address}</p>
0 g7 b. i9 J2 u k! j# [/ G+ w4 k% R - <Button onClick={() => close()}>Disconnect Wallet</Button>; v5 T: B0 X" u8 O% F) Y
- </>: r4 Y5 }2 l5 z& U. Q
- )}! t8 w1 a! g, ?: J
{0 @9 E$ o1 y; z- </div>
8 v! P% g3 V3 O8 ? - );# _" g' m/ S I) U5 K% }
- };2 K; n2 N$ A$ \3 r7 c1 E X9 W3 R8 f
+ e/ P4 i. N7 V9 T6 E3 |* R- export default LoginPage;
复制代码 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|