|
本帖最后由 riyad 于 2025-3-20 21:47 编辑
" ], p* }+ b4 l9 ]. f7 Y3 `( V1 }9 T# J9 D3 z# Q, ~! \
在前两篇教程中,我已经完成了红包智能合约的编写、编译和部署到 TON 测试网。今天,我们进入第三部分:开发一个前端界面,让用户可以直观地与红包合约交互。我使用 Vite + React + TypeScript 搭建了前端项目,并集成了 TON 钱包连接功能。以下是我的开发过程和完整代码,欢迎大家参考!) X5 w+ `. a& b6 ^
4 h7 K" T) Z* G _ B1 c前提条件1 k$ }& M p; m0 o1 L) l9 O3 @
在开始之前,确保你已完成以下步骤:: [3 K5 V* C% s: ^
1 y# F+ a2 v! c/ i/ i- 完成前两部分:你已经编写、编译并部署了红包智能合约到 TON 测试网。
- 测试网钱包:用户需要一个支持测试网的 TON 钱包(如 TON Wallet),并通过 @testgiver_ton_bot 获取测试币。
- Node.js 环境:确保已安装 Node.js(建议使用最新 LTS 版本)。
& @' [: u6 N: e1 Y3 m+ Y# ]9 ?& B . z& x* J7 p" R* c! s
项目文件夹结构
" K @. N# j. G# u我使用 Vite 创建了一个 React + TypeScript 项目,项目名为 TON-RED-PACKET。以下是项目目录结构
/ y& e" l" Y7 ?/ y: d
0 Q6 C& j, Q- T% @0 M" O, `. r: W N' ]" J" @
' S7 k3 r1 [, e8 Z7 ]目录说明: {) a9 m p3 J1 i, w9 h& x" i
components/:存放可复用的组件。
+ R2 |: V6 \8 Y* D- WalletConnect.tsx:钱包连接组件。
- RedPacketContext.tsx:红包上下文,用于管理全局状态。1 G+ r O7 s! _; z5 Z7 a" Y# N
pages/:存放页面组件。3 P! L, O7 @: w& c2 i! b
- ClaimPacketPage.tsx:领取红包页面。
- CreatePacketPage.tsx:创建红包页面。
- HomePage.tsx:首页。
- LoginPage.tsx:登录页面。
* k1 }4 `7 \4 N8 ?; _ App.tsx 和 App.css:主应用组件和样式。
$ T6 m0 Q- t* Bindex.css 和 main.tsx:全局样式和入口文件。
* g B% W+ {! Vvite-env.d.ts:Vite 环境声明文件。* P% P9 \3 l7 G% o7 [: G' h9 u1 c
其他配置文件:如 package.json、tsconfig.json 等。
1 u( _1 m; J. n; w0 I# |/ U5 z步骤 1:创建前端项目4 m1 z2 ~6 J% v# Z3 r" P8 ^
我使用 Vite 快速搭建了一个 React + TypeScript 项目。以下是创建步骤:$ R, N( w: c: K2 v: H3 h/ |
+ e5 T* t7 x" n& `6 ]
初始化项目: 在终端运行:# S/ M$ t/ b2 q4 O
- npm create vite@latest TON-RED-PACKET -- --template react-ts
复制代码 选择 react-ts 模板,项目会自动生成基础结构。
8 a! `6 {' `$ q* v4 F1 o1 F2 d( B6 h; M2 J
3 F0 N& p0 M( a. M- s# H进入项目目录并安装依赖:
8 n% I9 g* U6 g- cd TON-RED-PACKET
7 c/ Z3 P4 L7 B2 J4 w - npm install
复制代码 安装额外的依赖: 为了实现钱包连接和页面交互,我安装了以下依赖:
Y* D; ~% Q9 b+ Z3 Y, H
/ F5 r3 v; i: O k! A- npm install @ton/ton @tonconnect/ui-react antd-mobile react-router-dom
复制代码 打开浏览器,访问 http://localhost:5173,你会看到 Vite 的默认页面。
' x* |" b# A% Y; g
$ x) c/ Y: x6 s' a& }
' T" p/ o/ ~$ X0 {$ r+ g0 Y# W, d* I- Q |
步骤 2:设置入口文件和全局样式# B- C1 B3 f$ U% Y7 v6 X: h
我修改了 main.tsx 和 index.css,设置了应用的入口和全局样式。
$ R1 p9 ]. d8 l7 J1 t L4 X6 y) I8 ?* p
2.1 main.tsx
2 k8 l3 v8 G3 I2 ^main.tsx 是应用的入口文件,我在这里设置了 React Router 和全局上下文:" @. H i; Q& t) N% a( s3 W
- import { StrictMode } from 'react'
5 _0 [0 n; |" G - import { createRoot } from 'react-dom/client'$ w5 s% a1 K% J5 K
- import { BrowserRouter } from 'react-router-dom';; x4 c; H/ f' u
- import { RedPacketProvider } from './context/RedPacketContext'; o; Y6 J) h2 o
- import './index.css'
& z2 F$ Y5 V! P: k; j2 c& s. e, z - import App from './App.tsx'
: \1 F1 w& z- Z8 @) L$ L1 T
) R! i# R( T" T5 M$ J% \5 R% p6 J- createRoot(document.getElementById('root')!).render( }) G1 |$ u9 {. S
- <StrictMode>/ o) w4 S0 H; e" _
- <RedPacketProvider>
( v6 P& e+ A2 ]! }4 p+ _ T/ @ - <BrowserRouter>0 e5 `5 K8 X8 V$ p1 `+ x: J
- <App />
- y7 h% L/ h; R$ u - </BrowserRouter>5 y! W: x3 d: a
- </RedPacketProvider>
$ o) l6 @' J, c' k# P - </StrictMode>,
5 A: H6 u, H5 b t5 @( V - )
复制代码- :root {
9 T) }6 v1 ~8 O. K6 p5 K - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
5 L0 w8 q( A1 O$ @3 Y6 o5 ^ - line-height: 1.5;6 [, Q% j# E8 j% p( [( d# I2 R0 s
- font-weight: 400;7 P) W J6 j7 r% {% S
- ( x7 R, J, K2 y4 {0 j
- color-scheme: light dark;
$ g& ]0 q# h8 U- [. q+ n' N/ s: W - color: rgba(255, 255, 255, 0.87);
7 \8 z% I' t& S0 ?/ r - background-color: #242424;
' |6 i7 x7 w, M0 ^; m
% x% h% C! F! k- font-synthesis: none;
% j) ^2 F# r+ y1 w9 v - text-rendering: optimizeLegibility;) R$ l& [6 _8 K
- -webkit-font-smoothing: antialiased;
& T$ y* W* @7 Q$ F3 R; Q) a7 D - -moz-osx-font-smoothing: grayscale;
4 y* H. L# z. p+ J! Z$ B% L; J - }
* `; Z h6 B% w! Q
% s$ Z: P) c) A0 G* |, K- a {
- ?: \$ J* H2 Y/ K, X, J$ E/ E' U - font-weight: 500;
' d& f& d& @! u& f: B+ {# R5 i) w - color: #646cff;
3 |6 B# ]/ g2 w( ^, J0 Y - text-decoration: inherit;
6 P- Y( a% Z9 \& H - }
2 ^9 D _4 v. N( R* P - a:hover {5 m3 H+ Y- k3 g9 t0 @ j* u# w" u
- color: #535bf2;" g" r4 C$ J5 a$ O* n6 u
- }
; x- d! z% `9 }( S6 s1 @5 T) {
* H( a3 x/ e2 K+ l8 R/ G- body {- P# q1 A. z9 X+ K4 B) X
- margin: 0;
' `9 q9 ]8 T& [2 N - display: flex;
1 E8 u# Q" n8 t9 B& a" G7 x. X - place-items: center;
& o: V* ]3 y# S9 U8 ^- i - min-width: 320px;3 b: v+ K# d6 w: S! i
- min-height: 100vh;
0 j6 j9 h3 g- z, v - }' _1 ^; G0 w: O9 | d( g
/ a+ `6 e8 V! t- h1 { e. @5 S, [( W8 u
- font-size: 3.2em;
' I+ e$ E6 o6 j# D( U7 K7 ? - line-height: 1.1;, u/ g/ P6 S! S. D0 L5 N
- }
& ]4 x$ D0 e+ j9 j+ `6 t# c - , n7 ~4 Y. e* i- Y$ q. A- L# q+ S. y
- button {
[4 |6 I6 f; j. D& @ - border-radius: 8px;
6 a0 \ \+ d8 H: a+ A8 u! A' ~/ [ - border: 1px solid transparent;
0 y2 O9 g: z$ V" G6 I) x - padding: 0.6em 1.2em;
( v3 e2 O2 @) a+ C$ T# ^/ x" M8 E: q - font-size: 1em; _: d' L7 j9 n1 H4 R
- font-weight: 500;% h4 _8 v7 W% C* [5 |. o; R
- font-family: inherit;
& u3 ~+ g2 [ n z. x4 W* y - background-color: #1a1a1a;
6 ?$ q; F3 G b. a/ v# u+ W - cursor: pointer;( p) ?+ ~ y6 e0 P& h5 \# r
- transition: border-color 0.25s;
, P/ i: \& p+ x; b - }* G# x% U9 o m l! N2 c
- button:hover {0 \# F! x* H1 r, h$ I7 Q
- border-color: #646cff;6 `4 Q! c- p0 V; O3 i' l
- }: @4 H" e" E8 b, @2 n& A
- button:focus,
9 ~! }4 _ J6 q. y9 P - button:focus-visible {
/ c! d; |4 H, f- n a9 [ - outline: 4px auto -webkit-focus-ring-color;* V) }% O! t" X* \6 ^# H5 K1 ?) Q5 P
- }
, W9 V- v! I) D- @. A - " f5 q! P) ^( S+ C
- @media (prefers-color-scheme: light) {* O) [# _7 c* D
- :root {
9 B0 p+ t% {# P, B# S - color: #213547;
4 o1 s, I1 _2 Q5 y; G' o - background-color: #ffffff;3 r4 {$ M W" I. ?4 ^8 P; ?
- }
, \$ V6 t5 q6 y; X/ m0 O - a:hover {
1 K5 q+ P& R* t! t# m, w - color: #747bff;: O! M/ x1 C2 k+ o' O o
- }" z( w) E2 M& u+ ^$ a$ R
- button {' Q' g% R9 ?( [' a0 U& W
- background-color: #f9f9f9;
& q3 i7 T+ S9 x: h - }
3 Y9 Q6 M3 m- o - }
复制代码- // WalletConnect.tsx
0 T. M) j/ q+ @/ K3 K - import React, { useEffect } from 'react';
; e6 M7 F: m3 [) j8 Q, @$ |) ~ - import { Button } from 'antd-mobile';
# Z# t# h6 F, l' ]% \ - import { useNavigate } from 'react-router-dom';
5 m' i. M) o% v9 X - import { useTonAddress,TonConnectButton, useTonConnectUI } from '@tonconnect/ui-react'; // Import necessary hooks
# G2 I a# {. D: U8 q V- v& t - 4 Z8 P* W \0 y Y
- const WalletConnect: React.FC = () => {2 C6 |. v1 |9 K( u8 e$ y
- const navigate = useNavigate();
2 i- X, U8 O' i3 d7 u. F, L - const address = useTonAddress(); // Get the connected wallet address
" I/ S A$ P/ o0 u6 c0 k! ? - const [tonConnectUI] = useTonConnectUI();
* i9 i9 E5 J* T Z" `2 D
' O& Y, c+ J' a# `8 Q- // Redirect to HomePage once connected5 n+ y6 i9 O& x! K5 f7 N
- useEffect(() => {
& K; ^- U; |1 a2 K5 _ - if (!address) {
7 z( C5 C4 q" D - navigate('/login'); // Navigate to HomePage when wallet is connected
+ T# L0 P4 I4 d' Q3 O - }6 o3 m# R& K. u$ l2 w. @7 H, x
- }, [address, navigate]);
+ T1 m8 z" W: V* g. y" l! f' W - ; c$ l5 `2 h- L* w: [" R1 F4 }
- // Handle Disconnect Wallet* C9 I0 q6 K, t8 l4 s7 g% n
- const handleDisconnect = () => {+ q3 _; M- _# [
- tonConnectUI.disconnect();
; c+ t; H* h: b4 y, h0 x - navigate('/login'); // Redirect to login page after disconnecting
2 f3 W% \1 N3 U( v - };
# F. `0 A: g( h/ J; O* Z, _
, T6 X0 O8 n7 w- return (
$ g5 C# ]/ Y9 e, \6 [2 S - <div style={{ padding: '1rem', textAlign: 'center' }}>
- A9 d0 E2 ]* q8 @ - {address ? (
Y$ I7 Q- p9 C Q" o - <>
+ o) ~* b, z+ Q2 e3 Y - <Button onClick={handleDisconnect}>Disconnect Wallet</Button> ; D5 f* q/ o* R# U3 \- Y
- </>; M& V( R% }' b8 X9 ` O
- ) : (
! ^0 ]- c; M5 R/ G9 l3 O - <>
5 `3 D# {+ G$ h+ p5 k. _3 q$ b. i6 n - <TonConnectButton style={{ marginBottom: '1rem' }} />
5 Q, c' ?$ ~) ]- L - </>( i1 |0 s' B5 U1 t4 v- v
- )}
& u! R; W) _6 @# S3 L+ d" n5 x* N - </div>; U5 |* J5 {3 U) W, i$ i" B
- );
; [9 T c! y/ S0 f# @% x ~ - };/ a. o' ?$ B- e% M
/ a" F Q* Y1 v- export default WalletConnect;
复制代码- import React, { createContext, useContext, useState } from 'react';
6 ?. A7 C) z8 B5 L( ?# B( s - 7 _ K* J; k6 [8 Y: r9 z
- export type RedPacket = {
; Y4 l$ m. C3 L - id: string;
! S" p5 w( V2 W - amount: number;- C1 k2 r; L' }
- quantity: number;. z+ E* o+ B% m: W2 x% E
- message?: string;
0 Z! N2 R+ `; p& r' |! R - };
/ P0 N6 ^( n, o/ ^) { - # g1 D3 f# i$ f m& R6 ^" R
- type RedPacketContextType = {
8 @! T# x- o$ V; k! S' Y* L - redPackets: RedPacket[];
3 {. k' t8 e a8 [( L8 z - addRedPacket: (packet: RedPacket) => void;
. Y5 x% q' H+ c ` - };' t- O, D) i1 a0 G
7 B) U" M6 k6 A4 H& C/ C; ]- const RedPacketContext = createContext<RedPacketContextType | undefined>(undefined);
5 ?# ^6 P+ [/ H; o
+ b9 z% r' M9 b9 i" o7 S4 F- export const useRedPacket = () => {4 S8 x& Y! z8 j; o$ w. A L
- const context = useContext(RedPacketContext); M7 g4 S5 E% k3 L+ z( r) U1 k1 X
- if (!context) {0 h0 O+ v4 x! M$ U: g* R4 z
- throw new Error('useRedPacket must be used within a RedPacketProvider');, l/ I# m& m% v' w/ w* w
- }
2 X& f" x0 x1 }- [ - return context;
% |0 C$ P8 H9 \2 u2 m! J - };
3 C& A7 [2 X0 `' A4 u
( T7 H+ y( T9 `5 Z! o4 {7 \1 p- export const RedPacketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {6 Y% b6 C( k1 t/ o$ {. s; S
- const [redPackets, setRedPackets] = useState<RedPacket[]>([
! U' A1 a! q( ]$ z$ N8 A' ? - // Initial dummy data
) q) h& @ h: ]$ L - // { id: '1', amount: 100, quantity: 10, message: 'Happy New Year!' },/ D; w& D+ r' u/ a) j
- // { id: '2', amount: 50, quantity: 5, message: 'Best Wishes!' },
" z' s4 l/ L# e; N - ]);
3 f$ A9 r- e; ?8 S/ y2 D - & N0 q5 T! |' \$ L- v; a5 U
- const addRedPacket = (packet: RedPacket) => {
/ o( i O5 z- Z" L6 x4 _/ H - setRedPackets(prev => [...prev, packet]);
2 g& O0 l. ?2 P* s# t1 N) [ h - };% G3 m& O+ s2 l: V9 T, g) R% Y8 @5 K
. |& [. ]; e( f1 G. x% s$ Y- return (
8 [ v/ l7 N* Y; J Y& r' [* H% L - <RedPacketContext.Provider value={{ redPackets, addRedPacket }}>
, h) [7 l% ?( N1 \) e' ^ - {children}$ A: `* v3 v" L/ T% I" p
- </RedPacketContext.Provider>
+ l4 j* Q( g: c6 z) r6 l5 P - );
. Y7 V; y) @# m - };
复制代码- import React from 'react';
% ]' h. F8 J. Z8 @' s2 ] - import { Button, List } from 'antd-mobile';' H; o; H# D) m
- import { useNavigate } from 'react-router-dom';
1 g. v! b* l1 S9 a* @2 x$ q+ I - import { useRedPacket } from '../context/RedPacketContext';- Z: j3 D0 Q+ v* U! e# N! J& k
+ ~: p ~# \* B$ A# i; ?7 D U- const HomePage: React.FC = () => {
9 G m: r& q0 [6 U4 e - const navigate = useNavigate();
9 |) c! M- G; Y4 h - const { redPackets } = useRedPacket();% U' K/ @* ^; ^$ o
5 X2 C: v+ R1 n( r/ O* m- return (
: _# F+ S8 E$ V0 k% j/ g' E5 R - <div style={{ padding: '1rem' }}>2 r3 U( W7 ?4 l/ x
- <h2>Home</h2>) u+ M, V% ?( I. J! p
- <Button color="primary" onClick={() => navigate('/create')} block>/ a# [& \: b9 E2 {* j9 Y/ f
- Create Red Packet* k$ x' y m: z- L O
- </Button>
1 R) |2 v- g3 q5 i$ o% {
0 w4 y3 c, q7 |0 f' o- <h3 style={{ marginTop: '1.5rem' }}>Available Red Packets</h3>
% Q+ g5 m" k( U b2 | - {redPackets.length === 0 ? (
5 Z% {" x3 n1 d& q1 r0 B - <div>No Red Packets Available</div>
% K, D+ Y5 T! g4 C& }0 P5 g - ) : (4 i* k: S) _4 I
- <List>
/ q% m' j9 U. [. Z: h" x( _) x2 F* f - {redPackets.map((packet) => (! E2 m5 C; y d
- <div key={packet.id} className="packet-card"># }, Q7 p9 {: ]$ ^+ R
- <div>
' o3 n0 g4 t- O8 C4 k; I* b2 G! ~) | - <strong>Amount:</strong> {packet.amount}
1 C0 d1 w* e4 }2 X! B5 l W9 S - </div>+ L: K" ?/ m* b' }, j
- <div>* @9 k7 n6 t4 \
- <strong>Quantity:</strong> {packet.quantity}
+ g+ o$ C v6 U' d" H4 b4 q2 x - </div>5 |: n6 L' o, M2 i0 @7 I
- <div>2 E6 |4 k/ O+ z d
- <strong>Message:</strong> {packet.message}3 Z& f T3 X8 Q
- </div>
7 _: ?, S% v1 @ - <Button) B2 L. N3 h% M8 e
- size="small"
0 W* q) e" e: } - color="primary"% L4 S4 x3 P; h8 q( b
- onClick={() => navigate(`/claim/${packet.id}`)}9 _' u: l5 G0 _! i1 ^: V* D
- >
6 }5 Q* {. k, w& X8 T; t - Claim* ]( G8 C# R" \
- </Button>5 s" q" C2 P1 N4 V
- </div>
7 E5 L9 @. \9 o: w6 r3 A - ))}9 x/ e: \8 U6 \ L
- </List>5 B; @) S+ E3 w% K# n
- )}2 R C( y! k3 m+ j* _! o d
- </div>( i# {9 F; p3 m; B' h# a% Y
- );
9 g7 U7 U5 l4 Z: B2 e: @6 E( B - };0 z, L& v, r, Z" e8 A, X
- & W2 d" F/ n7 I6 s ?: [
- export default HomePage;
复制代码- import React, { useEffect } from 'react';2 h# F! g) d; A* d! Y
- import { TonConnectButton } from '@tonconnect/ui-react'; ( ~# j* s3 b& B& G3 b0 q% K
- import { Button } from 'antd-mobile';
X8 S f* U0 E W: F/ O# m% E9 g) A - import { useNavigate } from 'react-router-dom';
7 Z- F, i* o. F* U t9 I2 I - import { useTonConnectModal, useTonAddress } from '@tonconnect/ui-react'; & @; G& w ^# X+ f
- 4 a/ `: u, G% K2 y- l
- const LoginPage: React.FC = () => {
4 l: _! F: g, e+ S - const navigate = useNavigate();
) }. J* q1 w& m+ C( c- A3 r - const { close } = useTonConnectModal(); . t+ A- \! a! N: ~
- const address = useTonAddress(); 6 w, H) H0 v1 Y8 V7 Q
# g) O, P* g% i# I& y8 {- // Redirect to HomePage once connected
% U! g( p6 |$ K4 m - useEffect(() => {' `4 B' B/ N$ `& R/ p5 J9 ~
- if (address) {% W2 K3 O4 s5 V' _4 d; R
- navigate('/'); // Navigate to HomePage when wallet is connected
) |1 B# g7 z g5 i( h3 @ - }# V8 j6 Y/ ]+ P$ P4 ?" q+ v
- }, [address, navigate]);
& z8 A( u1 f G& |9 ]& ?
. H1 }% A. o6 S' L- return (2 c4 l& i" o4 h* D+ U
- <div style={{ padding: '1rem', textAlign: 'center' }}>
! H4 ]( A' K3 q$ b' p - <h2>Login to TON Wallet</h2>* P# M, n1 M6 l/ ?! Q, F! x
+ f+ W2 q. _+ U4 R- ]- {/* Show only TonConnectButton for connecting */}) ^! o" p+ l$ n" P( Y
- <TonConnectButton style={{ marginBottom: '1rem' }} />
2 y/ x1 ~( M7 P2 }2 K
1 H! _) N0 |. r( f- {/* Only show disconnect wallet button once connected */}
( x, i# g, `/ e8 F; F+ N$ { - {address && (
$ @# x: N1 o: r: ]3 J - <>
6 e# ^% ?) R$ b, a" ^4 w# U# ? - <p>Connected Wallet: {address}</p>' ~, i6 A! U% O
- <Button onClick={() => close()}>Disconnect Wallet</Button>. Q1 s5 f3 m+ c9 D% J
- </>* k3 C7 p' K; H
- )}
7 k; B5 b& r: f/ z# F- [0 s0 f
" G R$ G: f6 ^" @- b- </div>
3 K" J L) x9 J* w3 X - );
8 U7 m/ {1 f, [7 S4 [ - };
* N2 [/ ?2 |0 o& a# M - , A1 M) [; H2 E8 U; t
- export default LoginPage;
复制代码 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|