|
本帖最后由 riyad 于 2025-3-20 21:47 编辑
p' y4 o5 S2 U" L: x0 G- Z+ g$ i& G( B" z
在前两篇教程中,我已经完成了红包智能合约的编写、编译和部署到 TON 测试网。今天,我们进入第三部分:开发一个前端界面,让用户可以直观地与红包合约交互。我使用 Vite + React + TypeScript 搭建了前端项目,并集成了 TON 钱包连接功能。以下是我的开发过程和完整代码,欢迎大家参考!
5 Q. {# u! `- A. s7 k* n& T
' x2 X3 u; l" }3 E! `# b8 Z前提条件8 E! m; m8 n1 t7 p. i# g F
在开始之前,确保你已完成以下步骤:
& u/ Q7 L2 x2 y! D, E5 N. ?9 `# ^4 G; Y+ b7 D2 o6 V( S4 a! ^/ m7 }% C0 k
- 完成前两部分:你已经编写、编译并部署了红包智能合约到 TON 测试网。
- 测试网钱包:用户需要一个支持测试网的 TON 钱包(如 TON Wallet),并通过 @testgiver_ton_bot 获取测试币。
- Node.js 环境:确保已安装 Node.js(建议使用最新 LTS 版本)。
& I. I, T3 \' x/ ^2 E
3 r. b* S( {# A1 D/ @( {2 a( C, }) Y0 v项目文件夹结构 ^4 e f! j' d2 C- }9 H/ M
我使用 Vite 创建了一个 React + TypeScript 项目,项目名为 TON-RED-PACKET。以下是项目目录结构3 I* F1 A0 J9 k$ b2 N$ R, y
, V, ]- R7 N* O2 ?
3 e# J2 a! I9 ^
( l' a" e5 ?$ o9 k q" ^6 _- i
目录说明:9 o- K$ l# T" b* s5 |: U) v9 k
components/:存放可复用的组件。) D* h: L; A( J; m7 H J
- WalletConnect.tsx:钱包连接组件。
- RedPacketContext.tsx:红包上下文,用于管理全局状态。0 ]% M6 ]! g( E1 s. t
pages/:存放页面组件。' ?. H# g* w8 ]) \2 L3 ]
- ClaimPacketPage.tsx:领取红包页面。
- CreatePacketPage.tsx:创建红包页面。
- HomePage.tsx:首页。
- LoginPage.tsx:登录页面。
) X- x; ]6 B$ q8 _9 Z( @ App.tsx 和 App.css:主应用组件和样式。
8 _0 v: s7 w7 \% D1 Lindex.css 和 main.tsx:全局样式和入口文件。
" h' F7 r g# q/ c. h! i* Q2 n: Z$ Svite-env.d.ts:Vite 环境声明文件。- g: z/ H1 d' A3 I3 f+ g
其他配置文件:如 package.json、tsconfig.json 等。0 q5 G, J9 X% Y) I8 V4 r2 s" A; Y7 s
步骤 1:创建前端项目& X) x+ s: X5 x1 O
我使用 Vite 快速搭建了一个 React + TypeScript 项目。以下是创建步骤:5 N, m7 Q" L8 c r. A7 o
% N8 j# `0 h( e1 l8 L
初始化项目: 在终端运行:$ ?1 Y/ H0 u) F$ a5 I) i# V
- npm create vite@latest TON-RED-PACKET -- --template react-ts
复制代码 选择 react-ts 模板,项目会自动生成基础结构。
; F4 V O- \) O6 y+ {% ]. w- |' }" B
进入项目目录并安装依赖:
1 w2 H/ x! w* _: g- cd TON-RED-PACKET
2 S4 O' r+ m4 a - npm install
复制代码 安装额外的依赖: 为了实现钱包连接和页面交互,我安装了以下依赖:0 R$ c- ^! q7 a5 |/ v% l8 i+ G
* O2 ^$ c1 F, g) |5 o6 D: p- npm install @ton/ton @tonconnect/ui-react antd-mobile react-router-dom
复制代码 打开浏览器,访问 http://localhost:5173,你会看到 Vite 的默认页面。2 ^! D0 }+ j6 U+ x1 }. a$ B
9 j+ A0 c) P! Y' R
: a! x1 _4 x0 k, ~& {: f* `
" |, Z. p4 f9 F, g/ v7 \步骤 2:设置入口文件和全局样式
1 a4 ]2 N. m5 P( I我修改了 main.tsx 和 index.css,设置了应用的入口和全局样式。
7 o) x: Z* W0 h8 T
. P& H- \+ g7 H; J& [' G% U! H2.1 main.tsx
; w0 Z* S2 t% T smain.tsx 是应用的入口文件,我在这里设置了 React Router 和全局上下文:/ }! B( J% d$ L7 Z& y8 j/ Q6 z# z
- import { StrictMode } from 'react'$ }8 x7 b9 f+ P
- import { createRoot } from 'react-dom/client'* `1 t; |5 I/ F$ q7 j5 q/ y7 g* |
- import { BrowserRouter } from 'react-router-dom';
: L0 E* N8 { u- Q$ d/ H) y - import { RedPacketProvider } from './context/RedPacketContext';( V- g# @0 K) |5 @! z3 c# h a
- import './index.css'
5 {2 C) ~/ U, p$ g4 c - import App from './App.tsx'! i2 U9 ?& \2 k
5 E; d# p3 ]5 A1 h0 Q Y- createRoot(document.getElementById('root')!).render(+ W- @3 H8 ~ n8 ~: k! Z- S
- <StrictMode>
, t, H7 w" l5 d R2 S0 [ - <RedPacketProvider>
$ J" r4 m. F, X! K) l8 N - <BrowserRouter>
$ p1 ?+ |. K: I$ g9 W - <App />; t# l# ]! V% B& O# g3 A, a, {8 h
- </BrowserRouter>9 h& }6 U5 ~) l8 ^4 h, H/ a
- </RedPacketProvider>
# x7 h3 Q( c; [+ N/ x v - </StrictMode>,
& i! N9 w' I) l0 g; E. l - )
复制代码- :root {' }! S3 _: ]2 _
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;& `* A0 A' n! G$ \% C
- line-height: 1.5;, [2 T% o$ j2 H$ a2 W
- font-weight: 400;7 x* J: e2 {6 ^9 \& U0 B* z
- + R# c& N; T; O! G
- color-scheme: light dark;
& V+ X3 u% ~2 `) J. [1 [7 | - color: rgba(255, 255, 255, 0.87);, c( C0 @& c6 K* k
- background-color: #242424;7 }, v4 x# `5 Y) V: s9 Z
- ( o4 E6 H; s9 p( i/ K- m
- font-synthesis: none;
' L! Y" F, R6 w3 X - text-rendering: optimizeLegibility;
" p+ c/ [3 c$ Z' K - -webkit-font-smoothing: antialiased;
c$ j7 f: m8 v7 y9 E5 M - -moz-osx-font-smoothing: grayscale;( |7 C/ T' P( Q1 N9 K
- }' w( X$ ]( l/ y
# V5 Y1 K7 R3 d" T- a {7 p0 N0 d; c# ]3 j' M$ _* U8 B
- font-weight: 500;& r9 @4 V- i1 ~. `4 K1 o9 r. c7 n
- color: #646cff;
0 q$ g3 ^3 C4 }* M; x6 e - text-decoration: inherit;! B( u. V9 H9 ?5 X! j% r
- }
% ]' r b' {# v- I g# D9 X - a:hover {
; Z* f+ t: |- ^) m8 X2 A1 ?5 @+ x - color: #535bf2;
! H& ~) \. i9 E; S) p& ]& ~0 @# E - }
5 L! r. f8 R3 Y - % \+ y! b; t8 c+ d/ U
- body {3 W# B I; L+ a" J3 v) n
- margin: 0;/ t- w( A H, ~7 K
- display: flex;
" R/ d% B# d6 M% M" l- J! u; ]; T$ @ - place-items: center;. V) A4 Z. I7 T6 g2 V6 }9 |
- min-width: 320px;* Q: Y/ l7 _$ _. ~
- min-height: 100vh;
2 ^+ v7 A) y9 N; F( |5 w - }
3 D5 J6 \! b% P - 4 v$ ~0 t4 P" D2 n- k2 h1 `
- h1 {
. \$ R) @, T+ B/ [ - font-size: 3.2em;8 l" h2 G" ]0 Y5 p+ G
- line-height: 1.1;9 B Q# C" V# Y7 q/ L; Y) V
- }
9 e5 _0 d2 n7 ]- l8 f9 u - 0 S5 x) J" i9 E, P* |, o
- button {
2 Y ~, X+ L$ l4 z% o - border-radius: 8px;
" R; P$ I( }0 s1 y& }( y6 z" b - border: 1px solid transparent;. A3 ^* m) l/ s/ B& @3 f) }
- padding: 0.6em 1.2em;5 j4 s$ k* n- p2 v$ D& [8 D
- font-size: 1em;. W, C/ v, U' E9 [9 N
- font-weight: 500;
( B5 C# C9 Z) Q: K+ c' `% u - font-family: inherit;
% Q1 Z5 h, x; ^/ S- v0 t1 p* d - background-color: #1a1a1a;- i& y# e" s/ I
- cursor: pointer;
* Z. g8 o% v5 {3 R5 q+ `8 { - transition: border-color 0.25s;; }/ Z( I' q4 A2 t% E9 {0 E
- }% D. a& _5 B& z' {
- button:hover {: }: ^+ T% c& |3 v: D
- border-color: #646cff;
5 j! b7 u2 z/ g U* o! N$ k+ d; u - }4 r6 {% H$ d" g2 z
- button:focus,
. ^! P- s5 E! O - button:focus-visible {
- b" v$ S) D$ _ - outline: 4px auto -webkit-focus-ring-color;
7 u/ Q* W9 x* E6 ~6 ~ - }$ p7 \3 g1 M1 y6 j: r8 O
. o% x: [, d+ B6 \( C/ z' L, b- @media (prefers-color-scheme: light) {( p* I: {; l5 m) w$ A
- :root {
% Z' I A+ ]' O: }# T - color: #213547;
: ]2 w0 ]* B; Y( Q - background-color: #ffffff;1 {8 C! q6 q: u! K- M' v
- }0 H# I1 U. F2 q, M
- a:hover {/ u. \% j) j6 y+ c d
- color: #747bff;
1 R$ M4 e/ t/ @( T - }
, o$ t. m3 d) ~0 Z7 i - button {) ]0 \5 v8 m) D2 I+ R
- background-color: #f9f9f9;
7 }9 r. H$ o' W7 B# A - }
! ^0 [4 x. y% {8 t: D/ S; y - }
复制代码- // WalletConnect.tsx
, g; o# L! E5 P. a' k, _ - import React, { useEffect } from 'react';- I2 T- K. I$ S3 T$ P# n2 V
- import { Button } from 'antd-mobile';
0 t4 e/ ~, \/ ? - import { useNavigate } from 'react-router-dom';3 v, z( ]+ l' |9 ^7 V7 e
- import { useTonAddress,TonConnectButton, useTonConnectUI } from '@tonconnect/ui-react'; // Import necessary hooks" V: i- n6 k2 O5 m
! S9 X- H: }/ Y9 ~* c- const WalletConnect: React.FC = () => {2 e8 ?0 d+ d% l# p
- const navigate = useNavigate();7 Z8 Y8 w. r# w$ s7 [; @
- const address = useTonAddress(); // Get the connected wallet address
`# c0 M& t1 g2 l0 Y2 { - const [tonConnectUI] = useTonConnectUI();0 {4 k; \1 D" K$ E, H5 X
- * J$ D7 t3 \! w; N5 Z2 h' `7 q6 k
- // Redirect to HomePage once connected
% p W+ C- d, Z% R: P6 t8 `8 H - useEffect(() => {; O6 Z5 T+ M, m$ t5 a
- if (!address) {8 i3 N+ B9 L% d- o; `$ J
- navigate('/login'); // Navigate to HomePage when wallet is connected
9 \6 @4 N: c; C - }
7 j& N+ |( x) ?; t, N) Q+ t - }, [address, navigate]);: O d, l; d! c6 s6 [% O
- g) i- Y2 z4 U' z3 r1 \- // Handle Disconnect Wallet
2 v2 { y; e+ S6 \: Y5 i& Q. F - const handleDisconnect = () => {- Z2 c, a. q; J2 F5 i
- tonConnectUI.disconnect();; E5 h$ T# }) i/ m- @: ?& z( l
- navigate('/login'); // Redirect to login page after disconnecting
+ A: V4 }0 b# Y5 l( I - };# {5 C9 Y5 H4 Z: r9 X- ]
9 b9 \+ i, I1 ]$ ^" o+ }# y6 V- z- return (
) I% j+ x/ s: {0 `7 L - <div style={{ padding: '1rem', textAlign: 'center' }}>
^ u `5 U( c3 u% _8 s - {address ? (; ^' ^: O) O/ ] k& O# j% Z" |
- <>
8 N- n) \ v& @6 v - <Button onClick={handleDisconnect}>Disconnect Wallet</Button> 8 ?8 B8 _- r( Z' M8 {- ?
- </>7 u: i' T9 N: l, F9 w
- ) : (8 E/ _/ e7 q- b$ A2 T. v" ^
- <>
- G$ v0 n5 ?5 w3 ^( P' t - <TonConnectButton style={{ marginBottom: '1rem' }} />
% [, j: P C8 Y; q) P) G$ X6 W9 U0 ] - </>
- G7 o6 s+ @' l% \, ` - )}
( k, q9 ^. y+ b4 E+ C - </div># c8 X3 r: }" b6 {5 H2 W$ t, }; C) S* [
- );! u* e5 L' |, e. o. w
- };
" Y" r+ p6 V8 o; w. S
9 X, z9 r0 J2 h8 q- |* ~- n' l- export default WalletConnect;
复制代码- import React, { createContext, useContext, useState } from 'react';
1 k! @. X+ s; A* Q
4 T5 x: h7 o6 N& X7 R6 S' t- export type RedPacket = {
2 R3 x) T( Q! Y - id: string;
' U6 x! E/ G0 j4 y7 J - amount: number;
; @5 {4 A" d9 S. \) L7 { s" G - quantity: number;
: _9 T: ]8 R9 j9 F8 u. j: A N - message?: string;
0 ^% [4 }% @/ v - };( a$ Y+ [" k+ O) V$ k: r
- 7 F" P. k' E! Z) e' s
- type RedPacketContextType = {
" L3 a& D% h/ ] - redPackets: RedPacket[];
0 Z5 B2 ], z: r+ ?, r% n2 z - addRedPacket: (packet: RedPacket) => void;; A" |( }% q7 }1 x, a0 J
- };
9 p. {: `+ h' g$ [4 t+ X& T# u& K - 5 M8 Q. ?6 d1 k4 C* M
- const RedPacketContext = createContext<RedPacketContextType | undefined>(undefined);
0 M+ A, ~; ^9 U0 m. p! E9 Y$ P+ k - 7 w6 @' e- `7 g, h% a0 E" P/ U
- export const useRedPacket = () => {; Y0 |. K2 X0 i; Q
- const context = useContext(RedPacketContext);: c8 `. e5 W- Q: |" ^' l( o
- if (!context) {, g; s# n+ I j1 p, U3 @% t
- throw new Error('useRedPacket must be used within a RedPacketProvider');
2 j7 w, F) H8 h) e$ Q4 x( o; d - }
! H6 Y2 ~9 Z- k - return context;
& @. Z( E/ A* j- D5 V - };
) I7 F0 U4 \& L7 E. _4 I - ! E4 F* Z* j& J% `
- export const RedPacketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
( T: h. z; E1 P- q1 d% F4 F: T2 E - const [redPackets, setRedPackets] = useState<RedPacket[]>([
3 L) P2 ^, K9 ~5 ?" I6 N - // Initial dummy data4 ]0 ?7 G9 E0 Z5 S! j
- // { id: '1', amount: 100, quantity: 10, message: 'Happy New Year!' },
+ Z' ?. l" z7 R3 P* h4 h - // { id: '2', amount: 50, quantity: 5, message: 'Best Wishes!' },; @; D# g! Z8 W
- ]);
6 u" A2 O- G) v1 p" S) z F. | - % p# C. ]" W: ~7 T* R4 ]( U
- const addRedPacket = (packet: RedPacket) => {9 O6 \, X J3 ?& J
- setRedPackets(prev => [...prev, packet]);0 F% k0 K7 x$ w! g% k2 t" Z
- };
% |/ j- k, L. m2 y
# X, f8 W5 o0 B- W0 ^% u- return (
4 z4 ]. C9 s6 D( S T0 Q - <RedPacketContext.Provider value={{ redPackets, addRedPacket }}>
1 `6 |, i' w/ B6 z - {children}
% x0 k$ P% H+ i. H0 i; o - </RedPacketContext.Provider>& D' J: \5 p( Z6 g8 y! [+ E3 p
- );
4 [2 c) Y5 W/ d# d5 c - };
复制代码- import React from 'react';$ u$ S Q6 B! l1 {% @. F5 J
- import { Button, List } from 'antd-mobile';7 E' b. m- U! A: @9 H
- import { useNavigate } from 'react-router-dom';
7 v# P) ]- m" k5 O) k - import { useRedPacket } from '../context/RedPacketContext';% Z/ I2 l1 o3 H5 y: d* A
- 0 E2 A; }' r0 g/ F5 ]/ f
- const HomePage: React.FC = () => {
d* H5 J5 R" Q: n' t6 s9 h, B - const navigate = useNavigate();
7 V- Z- V) b2 z6 m. A - const { redPackets } = useRedPacket();$ _/ O# P1 G" ?* M) |0 }8 E
- 0 h& b% n2 w: c9 f" T8 `
- return (
0 u5 r% a J0 l3 l) o( m - <div style={{ padding: '1rem' }}>! w& t% ]+ B) C
- <h2>Home</h2>3 n1 V; B- a( v' s- m5 z
- <Button color="primary" onClick={() => navigate('/create')} block>; X; R8 [9 A1 v5 ^% G7 B
- Create Red Packet, h8 I# K1 M/ ?4 ?) v: z
- </Button>
8 j7 A6 h3 @0 J& U3 q - - y1 ?- V7 i; }% [- \
- <h3 style={{ marginTop: '1.5rem' }}>Available Red Packets</h3>
% w# k, N3 h0 i0 A - {redPackets.length === 0 ? (3 [' G2 `( E; J9 N1 ]; W- _
- <div>No Red Packets Available</div>
- G- e- s/ z4 i - ) : (
5 ?" {* b; }; {" I - <List>
! C# |- g; v; \; P% \7 e8 ^ - {redPackets.map((packet) => (
2 B b" _4 J$ K1 @. z - <div key={packet.id} className="packet-card">. F& u* K$ b% Q. _! N
- <div>9 [2 j& x7 g7 j
- <strong>Amount:</strong> {packet.amount}; ~& w+ n8 m8 @& F6 I! v/ v& @
- </div>% k6 q0 J& @4 N6 F/ |, Q! q
- <div>
$ r% Z' O& b' K6 S - <strong>Quantity:</strong> {packet.quantity}
4 e: _ W5 {" P8 n1 \+ k0 ` - </div>9 P c2 G5 X7 s4 D% [( A
- <div>; a% m3 ~: r, p9 ]/ t
- <strong>Message:</strong> {packet.message}
% V# } X8 ]2 l6 _2 b, {* v+ o; h - </div>
2 }. f1 K+ w [3 A - <Button8 D, v' _8 F) }/ X3 s
- size="small"
- Y& M: c" e) m8 G - color="primary"
( u9 v! H% b# k" C - onClick={() => navigate(`/claim/${packet.id}`)}
3 }, n6 o4 R1 s& C% a$ e8 w - >
6 q1 w/ C; A6 B - Claim
( k! K9 d, ^# P+ U5 [& d - </Button>
7 Y3 M5 u2 \6 {6 S9 R8 P# F& a; [8 X6 \ - </div>
. z8 ^- Y" c" N1 @$ J+ ?- g3 u; z - ))}) n2 Y( z' @) e6 k9 G
- </List># r* A! D: {1 F' w }
- )}+ X1 ^( J% I+ U# o: g) U
- </div>0 R3 ~9 k( `& r1 J) E' t% Y" r
- );& Z/ p4 [* b/ k! Z# }/ C
- };
. H7 P+ h7 b. I3 |' k5 i3 s* P9 J
( e$ f" x9 B7 a! Q6 F2 m; R- export default HomePage;
复制代码- import React, { useEffect } from 'react';
8 d0 B) Q0 J2 n4 V - import { TonConnectButton } from '@tonconnect/ui-react';
) S! Y4 O5 I0 m% l; H# R - import { Button } from 'antd-mobile';8 H; l7 I- d! L, E1 L* @
- import { useNavigate } from 'react-router-dom';
* p2 p I% T' t; W - import { useTonConnectModal, useTonAddress } from '@tonconnect/ui-react';
9 x6 a; H8 r4 i V1 u+ N. H - $ ~* B* ^" G6 f, _6 x6 {
- const LoginPage: React.FC = () => {' u1 i4 y: Q9 ~( \
- const navigate = useNavigate();6 T H9 D- ^ Z
- const { close } = useTonConnectModal();
% f; g5 f. r* w G2 {' f8 X - const address = useTonAddress();
/ s" e, C" R5 [" k - 8 y; K7 b5 N D& y3 F9 c
- // Redirect to HomePage once connected
p; d: p' K& i9 t: K' z - useEffect(() => {
. R. L& y8 \8 \3 j - if (address) {+ y! i0 g/ Q" j2 C2 G
- navigate('/'); // Navigate to HomePage when wallet is connected
' Q3 C- v$ H+ K - }" U1 {7 f3 K, I" E0 I( a c" d# d
- }, [address, navigate]);% G. k+ Z# E, c8 P
- ; q: h, u; u) { ?! K% _" ]. R
- return (
1 R8 e% K: b8 t& C c6 g - <div style={{ padding: '1rem', textAlign: 'center' }}>8 E* A8 `/ d& S6 h
- <h2>Login to TON Wallet</h2>6 }$ C8 e- H4 E' x1 k% K- S6 g, h* r
- & _/ t* u1 }! c; v! T# q
- {/* Show only TonConnectButton for connecting */}
% @1 ?/ v! r/ `$ S. q- v4 } - <TonConnectButton style={{ marginBottom: '1rem' }} />& y6 f3 R7 }* g' |: J \* x
3 ?, `% B; b" N$ t0 `6 a% t$ x) k6 t( Y- {/* Only show disconnect wallet button once connected */}) g8 f( P, \; b# H$ c: m- e( K
- {address && (
4 E: i' ?! y9 L* P3 E - <>
6 t+ G( b3 n' z- G* x2 X - <p>Connected Wallet: {address}</p>$ V7 ?1 @( P/ K! V" D- |( F/ A
- <Button onClick={() => close()}>Disconnect Wallet</Button>
& l6 {5 q) z, d+ F4 t4 h* Y" W - </>. Y# ?2 v( F2 t
- )} M2 @/ n0 ] k q" K* I3 G* V& G
- b# M7 |' s4 i' f- </div>4 _2 {. S6 v1 x# a9 ~" Z6 }( u
- );
: l$ q0 s, ?( s - };
2 i9 s# D& A ]4 t' A$ G
$ E8 ~. ] x- f( X" h1 E( E) V- export default LoginPage;
复制代码 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|