|
本帖最后由 riyad 于 2025-3-20 21:47 编辑 5 _ s7 E" b2 q* V4 H3 @
* D+ W* V5 s/ P9 C1 f在前两篇教程中,我已经完成了红包智能合约的编写、编译和部署到 TON 测试网。今天,我们进入第三部分:开发一个前端界面,让用户可以直观地与红包合约交互。我使用 Vite + React + TypeScript 搭建了前端项目,并集成了 TON 钱包连接功能。以下是我的开发过程和完整代码,欢迎大家参考!' `6 S) x% O" s
7 T& x! ^+ t' e& X" H. `前提条件
" v3 x: s1 q( `: }3 K在开始之前,确保你已完成以下步骤:
) m9 e: y3 F, E' o3 o( z
' ]9 j8 X+ f9 n7 ]- 完成前两部分:你已经编写、编译并部署了红包智能合约到 TON 测试网。
- 测试网钱包:用户需要一个支持测试网的 TON 钱包(如 TON Wallet),并通过 @testgiver_ton_bot 获取测试币。
- Node.js 环境:确保已安装 Node.js(建议使用最新 LTS 版本)。' u- W+ }# T7 \4 ^0 _
" m! [! b- G8 o( c9 L
项目文件夹结构
3 y' I& n( J8 K- @9 j+ m$ c我使用 Vite 创建了一个 React + TypeScript 项目,项目名为 TON-RED-PACKET。以下是项目目录结构
* x) N: O* B9 l& n6 R4 R+ ~2 h# t1 d; L
, _3 z; w2 X, F6 L/ i! S- I4 @- N& j- M, v1 p5 m Q
目录说明:
- T' G9 s& o: V+ }3 a% ecomponents/:存放可复用的组件。9 {4 _3 V# V' g2 t. X& ]
- WalletConnect.tsx:钱包连接组件。
- RedPacketContext.tsx:红包上下文,用于管理全局状态。
% W) j+ z$ F* s% T' G. `+ }* M2 u; m pages/:存放页面组件。9 g7 D& r" y5 E( s6 f# Q! T
- ClaimPacketPage.tsx:领取红包页面。
- CreatePacketPage.tsx:创建红包页面。
- HomePage.tsx:首页。
- LoginPage.tsx:登录页面。) _1 `. L! C* k" L9 w) J* Y# N0 L
App.tsx 和 App.css:主应用组件和样式。
+ G3 b& ~/ } l+ Jindex.css 和 main.tsx:全局样式和入口文件。
7 x9 m: b* c; `# kvite-env.d.ts:Vite 环境声明文件。6 c8 W" V$ }$ u! H; u9 `! w/ Q
其他配置文件:如 package.json、tsconfig.json 等。9 U8 O, s& c) g" i& t- r0 H
步骤 1:创建前端项目
. X" Y8 R, s9 H: _我使用 Vite 快速搭建了一个 React + TypeScript 项目。以下是创建步骤:1 c2 H; g% T5 v8 F
# ~" r& u4 B8 R$ M9 i: ]% K3 k- Q
初始化项目: 在终端运行:" r+ v% p$ Q4 M9 j" g
- npm create vite@latest TON-RED-PACKET -- --template react-ts
复制代码 选择 react-ts 模板,项目会自动生成基础结构。
4 \, g5 U9 Y- j3 Y1 j
0 C0 M4 @: O/ c' D' F7 N8 o进入项目目录并安装依赖:
2 y& [. d! A6 y5 Z, E+ a, W- cd TON-RED-PACKET
7 |6 g8 n4 W$ n: f - npm install
复制代码 安装额外的依赖: 为了实现钱包连接和页面交互,我安装了以下依赖:
9 W1 l8 q: W1 J: _/ B1 B/ P
" N* [' @1 K% h' Z6 G5 M+ W- npm install @ton/ton @tonconnect/ui-react antd-mobile react-router-dom
复制代码 打开浏览器,访问 http://localhost:5173,你会看到 Vite 的默认页面。
( z! X! w2 s- C8 g7 G) Z
t+ o( z0 @; D
3 z5 u$ Q4 S2 d2 V% ^, l+ B2 |# d. E: U& o' z
步骤 2:设置入口文件和全局样式
0 s! Z, E: _9 d4 p8 O2 Z我修改了 main.tsx 和 index.css,设置了应用的入口和全局样式。
% n& C/ j' ^/ a' ]1 M7 {' \+ C7 V7 B/ N8 d! n: W
2.1 main.tsx
* p! V6 {) W* D/ W2 X1 ^5 Lmain.tsx 是应用的入口文件,我在这里设置了 React Router 和全局上下文:" g6 _4 F4 d- J7 R
- import { StrictMode } from 'react'
/ y; X0 ` m$ ]9 ^" L, `: v4 [ - import { createRoot } from 'react-dom/client'
' _0 S0 r) H$ B1 J( \1 @, Z0 C+ _* [) T - import { BrowserRouter } from 'react-router-dom';
- p" r, a" x8 W7 x! X - import { RedPacketProvider } from './context/RedPacketContext';
5 H* F" d% U% U3 |2 R) u - import './index.css' e: U* A4 I Z2 g0 \
- import App from './App.tsx'. ]: j( Q/ Y, j2 _
, j2 W# H3 ]! ~0 C- createRoot(document.getElementById('root')!).render(& O9 ?2 T/ K% @, G
- <StrictMode>
% g/ a% T3 r; Y F: \ - <RedPacketProvider>
+ T: P7 m4 G) Y+ X7 P4 [5 A& p1 g# Q - <BrowserRouter>) Z. v4 |* G8 M. M
- <App />
! a( O0 L5 q1 N* c' O! S- ^ - </BrowserRouter>3 g8 n/ S+ N7 e g- N. S$ o6 ?
- </RedPacketProvider>
- |- k; F3 E$ p' Y - </StrictMode>,
, k' }/ _) a: d- J1 N/ S - )
复制代码- :root {
. l& v: t) b0 A) ]) } - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ t: \ L3 X0 o! b - line-height: 1.5;
' n( f* t1 A3 I# } - font-weight: 400;
, b9 K( G: h; u) i( A+ U( }" g2 J
9 {% J- b3 g) U2 v- color-scheme: light dark;2 _" M5 p% F# `& ?, U# B$ I
- color: rgba(255, 255, 255, 0.87);* k/ W" u; ?9 U2 m E, `& q
- background-color: #242424;* [0 u; ` K6 O0 f+ F3 V
- ; n# n+ B9 C( {: K, K
- font-synthesis: none;) C9 {3 S- g( U# {
- text-rendering: optimizeLegibility;/ i e# S; Y+ h* P9 y: V6 c
- -webkit-font-smoothing: antialiased;
& }7 J" |+ v1 j3 p" M5 J - -moz-osx-font-smoothing: grayscale;
% U" q4 b5 V& r g0 R - }
+ r% X4 x& n$ i* Y - 0 L9 N) W1 w4 E
- a {" _/ r2 K6 }0 C Z; ~8 X4 P
- font-weight: 500;
) N. B/ T) |% w$ N - color: #646cff;% ~2 T/ e0 r/ ?9 W
- text-decoration: inherit;) z; t' K4 H" s1 g0 y
- }
1 _4 \4 y9 P6 g& I - a:hover {9 P( j' l% W" L
- color: #535bf2;
2 p" z8 c0 W3 j" J8 L& Q3 a - }
. E) ?* l8 L! C& Q2 X& l. i" c$ d
8 A8 f" U# l( b$ h& i- body {
; ~& M# M9 Q: S/ C- v I - margin: 0;1 t" j- ]2 t1 H) q$ F3 \2 M
- display: flex;
@( S( \$ d: q5 \; u6 m% E. } - place-items: center;, p3 W3 M r0 l0 ?7 Q% P
- min-width: 320px;# ]& K1 E* q: Y3 W6 U8 @. N: ]/ ]
- min-height: 100vh;" ?6 X$ N0 S8 t; s9 A3 T
- }
- X% {3 s1 C, P8 @
. V$ Z4 j0 n, ?$ M- h1 {8 ?9 F1 I" W. k+ M2 M6 Z7 V1 |
- font-size: 3.2em;
. J) P! {8 Y7 g8 X" L4 @ - line-height: 1.1;' {' T; {+ A" @; s
- }0 s6 w" A+ p& I
) h+ K$ f& t. Z- button {
2 |; A+ }; r* D1 _$ ~3 ^- D# u - border-radius: 8px;. |* [4 b0 |( z8 n( k
- border: 1px solid transparent;6 n5 U+ k8 }3 V; I4 y- F! ?
- padding: 0.6em 1.2em;
) |/ z9 j5 x( e, ^/ y6 s. q - font-size: 1em;
! X, p, c1 o8 u3 A4 j0 \ - font-weight: 500;, h. c/ U& W4 w8 G; `* V
- font-family: inherit;, o m7 G' u2 k$ S2 `" z
- background-color: #1a1a1a;: P' w# W2 F0 ?2 F8 }
- cursor: pointer;
1 }7 q8 t. E$ @% i/ F9 v7 p - transition: border-color 0.25s;6 l5 g+ ^( D& v
- }- N, t: ~" l$ ]! F
- button:hover {! v. c5 U4 a6 t: d
- border-color: #646cff;6 }1 O- w& C- [0 H! F! V
- }! q1 c* J0 _9 j1 Q
- button:focus,
5 b" n0 @& Q0 g! a7 `5 b2 V1 H+ \ - button:focus-visible {! }0 s' s8 d/ m( Z) g) ?
- outline: 4px auto -webkit-focus-ring-color;
& W: G* h4 u1 i - }* Y( q6 T( h5 q7 `1 S
- + h* Y7 m5 d3 B* n
- @media (prefers-color-scheme: light) {
, s3 K" D0 i6 d9 z4 } - :root {5 K$ e/ s6 ?/ Q: d
- color: #213547;5 Z; n* v. w- w/ q4 |8 \# M# ]7 l
- background-color: #ffffff;$ e# p5 \$ v6 Q& J7 C% G
- }! z: N' p! l/ n$ m" n8 ~
- a:hover {/ z8 j" W H* D6 K0 ]) l7 ~/ A8 V8 q
- color: #747bff;( m* n& w7 B& g' h8 D
- }
- x3 h2 u/ \4 N5 z3 `4 E: W - button {7 O9 Q: f3 ^+ j+ w4 Q
- background-color: #f9f9f9;! s& J4 J O; m! g
- }
- ]0 @! K6 ?- |) L4 k - }
复制代码- // WalletConnect.tsx7 [1 }+ p) d6 e, r& o0 d9 O
- import React, { useEffect } from 'react';, e5 o# ^5 Z9 v2 g3 r; R
- import { Button } from 'antd-mobile';% ]3 k3 j" o( A/ D5 K0 w" h6 i
- import { useNavigate } from 'react-router-dom';
5 a9 v" c6 Y% {7 ? ^. J K - import { useTonAddress,TonConnectButton, useTonConnectUI } from '@tonconnect/ui-react'; // Import necessary hooks
8 Y# |4 H5 o+ q4 ], m5 S; o - 4 X/ Z. s' p* g) f$ i
- const WalletConnect: React.FC = () => {3 D: o8 i+ L* A8 O' |
- const navigate = useNavigate();
* w+ O" t# m1 \% o+ \ - const address = useTonAddress(); // Get the connected wallet address
1 B* I" v1 y5 F0 j; o. K' \ - const [tonConnectUI] = useTonConnectUI();
7 i+ n4 b: c3 G; G% d2 R. u5 e
- v: e! l2 N; ~+ ~! Y- // Redirect to HomePage once connected/ x9 _9 `" n( r( C5 p* O
- useEffect(() => {: `/ S0 H/ v, `/ z8 {
- if (!address) {
, a/ J6 m4 [. R! G" \1 N9 D - navigate('/login'); // Navigate to HomePage when wallet is connected
. M' [ J0 q2 D- r% k - }
0 l+ M/ ^2 A" b! x J% Q - }, [address, navigate]);' }: e( D; H1 r) Y
- ( S. n0 p0 I! S8 Z7 o
- // Handle Disconnect Wallet! R8 ^: L+ b& X0 x7 P! ~) T
- const handleDisconnect = () => {
5 e# m& h$ \6 o$ @9 K8 D9 Q2 { - tonConnectUI.disconnect();! M' k0 W1 _* C9 _0 H
- navigate('/login'); // Redirect to login page after disconnecting
! N# ?' A* p& T" H - };
/ t- e1 |2 u6 x& @; h0 a6 B$ R; j: P
; A$ s% y& c8 R n# \( \ o- return (, W- M) r& P* W) u' n
- <div style={{ padding: '1rem', textAlign: 'center' }}>
( n2 I; O" ~/ f/ y5 K' z" w- Y - {address ? (
2 W" ]# c- v( l: e/ o - <> L U% b# F4 x% Q# }* F
- <Button onClick={handleDisconnect}>Disconnect Wallet</Button>
1 ^3 y; X1 z* [' o e - </>3 p) s X% M( K# P" ?8 P
- ) : (" b+ Z7 s9 I/ _' H
- <>, ]/ M7 L, e0 u( j
- <TonConnectButton style={{ marginBottom: '1rem' }} />
/ O! @: y3 E: { j - </>' y0 h* k! w6 V1 z
- )}
" P" a- S6 i# O - </div>
! ?1 q- ]" o- a6 W1 M - );
0 K6 Z- h6 R0 O5 v- r6 h2 m - };& o5 o( G4 o8 v5 m: F
$ q9 O3 f3 `, J" S9 u% l' V9 H- export default WalletConnect;
复制代码- import React, { createContext, useContext, useState } from 'react';
1 _7 q. ?) P2 G4 m2 d3 K) h- F# X - * O7 l6 e, v$ p8 H* X7 e- i- t& ^
- export type RedPacket = {
" @. r; h4 n9 r5 j6 H - id: string;5 b# A( P0 X/ q1 Q" S2 `2 E8 j
- amount: number;/ c. b/ Y' l0 |2 _
- quantity: number;- `7 e) h( S& L6 R
- message?: string;
% ]0 Q4 x/ k/ G0 T - };
, A3 B7 \% D. U) J- {9 E- E - $ G2 ~- B8 q! l }2 k+ a0 B. C
- type RedPacketContextType = {
/ M6 u' y1 b) p( W - redPackets: RedPacket[];
" F# }" e) o- U; Q - addRedPacket: (packet: RedPacket) => void;
3 i! ~' M8 R* X& r$ j2 ? - };
) M' M' R* f( p9 N3 k9 s
, x4 E& k1 Z* h, \2 e) V9 _- const RedPacketContext = createContext<RedPacketContextType | undefined>(undefined);
; C+ G4 m7 w4 K! Y
2 I# O, D8 P. s# k# T* [# U8 b- export const useRedPacket = () => {+ }' H2 n5 ]/ g. f8 j5 _9 |. s
- const context = useContext(RedPacketContext);1 J& d- l1 e. n- ?* o' v+ Q3 j
- if (!context) {
2 w' W! H0 C3 t" k; e) z6 O - throw new Error('useRedPacket must be used within a RedPacketProvider');$ B) P& u5 p, m) J& I" s0 c
- }
0 u, r& L0 h1 K+ b0 l8 |2 v. t+ k - return context;
5 O+ Z$ d+ Y- m5 ?) c8 { - };' Z; U% H9 Y: M2 q! }
5 a; R* R6 w6 i# E: m0 L: I& h% ]- export const RedPacketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { F! b8 G# D# }8 ]# ^
- const [redPackets, setRedPackets] = useState<RedPacket[]>([
& q8 p$ `; A# l2 F& l5 Z) {& W) ~ - // Initial dummy data
' s. a) i! R( e7 @- f0 w* ]# H0 B - // { id: '1', amount: 100, quantity: 10, message: 'Happy New Year!' },% A* y, i- J7 S7 D4 k0 G
- // { id: '2', amount: 50, quantity: 5, message: 'Best Wishes!' },
3 f% b5 r* t5 B% U/ u/ ~% X - ]);7 c' t% S- C' G; V2 @+ H
% i) S/ [6 r! z% t$ W8 ~- const addRedPacket = (packet: RedPacket) => {( U' U6 x- ~" A% R+ u& q' q
- setRedPackets(prev => [...prev, packet]);- j/ r& e( Q/ T3 c
- };$ `. I4 [5 x; o2 a( I% \, l: T ~
4 [3 d. Y4 o" A0 {- return (
3 N% M7 f9 e) h1 F# G2 {# U/ e - <RedPacketContext.Provider value={{ redPackets, addRedPacket }}>
1 v9 K8 L1 b3 p - {children}
( @6 ?1 O9 F6 V - </RedPacketContext.Provider>
4 z; n z+ r/ X" K8 S3 y - );
1 x' A, d7 @! E# e8 M9 |& ~( a9 G - };
复制代码- import React from 'react';' d1 k# {6 G5 h3 A8 R. |, ]
- import { Button, List } from 'antd-mobile';$ q3 Q1 S% n6 h7 M4 t
- import { useNavigate } from 'react-router-dom';, p+ G8 y% u* o( F. [7 y/ e
- import { useRedPacket } from '../context/RedPacketContext';: Z2 E7 N% ^7 f1 R- v1 n6 N& ^
4 Y Z. ?, O2 P( j9 z* d2 i8 w- const HomePage: React.FC = () => {
; f$ S! f1 @0 _, {2 i. p( S - const navigate = useNavigate();- z' O' S$ |( l6 g" m5 R
- const { redPackets } = useRedPacket();
. z: Y! d' ?) B5 P! E, c; m
7 ~7 l$ W; \+ _0 }+ c9 g# l( J. R, z- return (
# E" y- a' C. i$ B - <div style={{ padding: '1rem' }}>, p2 F% n( ?5 D6 ?5 w
- <h2>Home</h2>
7 \( h% E8 T A# X2 h - <Button color="primary" onClick={() => navigate('/create')} block>+ i3 y9 w+ G. j6 n, Q# \* X
- Create Red Packet
0 G z) D1 a. \+ R+ l; U, a- g& S - </Button>
& @) l% o" v7 G- p# R! Y7 I q' {/ _( x - ; G. a/ J* }. [6 X+ k
- <h3 style={{ marginTop: '1.5rem' }}>Available Red Packets</h3>, \- P7 m; P, x/ _. {
- {redPackets.length === 0 ? (' \8 n2 h4 L( n: v7 Q5 f
- <div>No Red Packets Available</div>1 A6 n" E4 C4 z$ A ]1 A! H" f
- ) : (
3 B0 M! i. v% b" X& @ - <List>" H$ q7 k( {2 `# G
- {redPackets.map((packet) => (
, M+ D% z2 g6 G" p T - <div key={packet.id} className="packet-card">' l; v& e! Y& o4 N9 T2 _4 X: k% g
- <div>
% s. E! X0 ?% n% O4 M4 A% {* E - <strong>Amount:</strong> {packet.amount}9 G/ @ ^- U6 ~$ X0 n" g4 T i
- </div>; |& _; S8 x5 G+ Y
- <div>
; N3 _4 Q: [! o. k' }+ W - <strong>Quantity:</strong> {packet.quantity}
) V% i. }. v+ | - </div>7 m3 N0 b4 P4 D! S/ l
- <div> l5 T6 D: `! n: P
- <strong>Message:</strong> {packet.message}: A( L9 v( d1 ^* q# \, N
- </div>
& L5 \$ M% g4 H8 E - <Button
7 E& a9 ?. @; F/ Z5 [7 V - size="small"
# }! ~1 O6 m/ n' H" f( H - color="primary"& @) u4 D" U% J* D
- onClick={() => navigate(`/claim/${packet.id}`)}
" e" g8 R$ ~4 Z8 D - >" E( |0 X# z, `+ M, E- i8 F
- Claim
( R, v- u' V% C3 u0 H - </Button>
1 m: w9 b( ?8 g - </div>
* D" H8 h- N# D - ))}
4 t3 x4 B/ `' c" f - </List>
, J2 t( `. I( |6 P( j4 G( d1 R - )}
w" l2 q! f' n9 x5 I - </div>/ C. ~. |6 u4 e
- );
' G" E. [+ Z9 ~3 x - };
- G8 L8 {0 b+ w L" E
- r# M" B* q; z/ \- export default HomePage;
复制代码- import React, { useEffect } from 'react';
3 _* ?- [' o6 R; @ - import { TonConnectButton } from '@tonconnect/ui-react'; 9 f; r6 N' O+ T8 r) w9 j
- import { Button } from 'antd-mobile';$ D( K! P9 y2 }& ?
- import { useNavigate } from 'react-router-dom';
: p' Q3 H- B) T& Q# m. N - import { useTonConnectModal, useTonAddress } from '@tonconnect/ui-react'; 9 e/ A5 \* M$ v6 [
- 4 _& }1 H j0 o2 t3 y
- const LoginPage: React.FC = () => {9 k/ x, C+ m+ S* }1 Y
- const navigate = useNavigate();
+ j2 X* q1 m8 [ - const { close } = useTonConnectModal(); 1 R, [! _, y6 v \% K- M
- const address = useTonAddress();
, D. ~6 `6 V: R2 S6 A* K J - - v1 H; E1 e/ i3 j. _& h
- // Redirect to HomePage once connected
3 I* [, x6 R8 d3 Y - useEffect(() => {
$ y0 P2 |; N" g5 J5 I - if (address) {8 y' c5 m1 u6 t$ s$ E" u1 `
- navigate('/'); // Navigate to HomePage when wallet is connected
* x! u5 ]/ U9 l0 e0 d - }
% V7 f3 A5 N9 ]. { - }, [address, navigate]);
. ~- D# q2 A$ S
9 i) _( ~/ E1 r+ S+ Y- return (
. D' O, \7 e5 W4 j: U$ w# `' L - <div style={{ padding: '1rem', textAlign: 'center' }}>
4 e$ Q6 ]. [/ C' }& v4 c5 K4 O - <h2>Login to TON Wallet</h2>
1 C/ R/ n6 m! g8 U# Y - ; ?4 h5 p( z% j5 b7 q9 `! o
- {/* Show only TonConnectButton for connecting */}
# n" i# v1 S' Q8 B - <TonConnectButton style={{ marginBottom: '1rem' }} />* j' f4 E* `: d2 b1 x' w' r( ~
' u( ]# _( T7 S5 [2 [( ~- {/* Only show disconnect wallet button once connected */}
1 V8 T$ y7 k: p0 b$ h. j - {address && (
8 W5 E$ f. ` v1 ^ - <>
+ b. H* o) N6 P - <p>Connected Wallet: {address}</p>
y8 Y$ `& u8 o( g9 ^ - <Button onClick={() => close()}>Disconnect Wallet</Button>
: L. ^. |7 t! U' C# g1 C - </>
1 ^9 H3 U* T+ @2 v5 S8 G - )}
. ~9 |, E9 @& t" b; K
5 p0 c: k1 b- w- </div>
5 P0 U, w: O$ I; L+ L$ I - );
! z9 b. B/ }9 p- n; k+ ]% E5 B1 b - };
0 k* _* {9 g! x j% W4 B2 l - / e' \! f) e( p9 \7 Y
- export default LoginPage;
复制代码 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|