pinme 2.0.0-beta.2 → 2.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1544 -962
- package/package.json +32 -32
- package/template/README.md +0 -177
- package/template/backend/package.json +0 -12
- package/template/backend/schema/001_init.sql +0 -35
- package/template/backend/src/worker.ts +0 -108
- package/template/backend/wrangler.toml +0 -11
- package/template/frontend/.env.example +0 -4
- package/template/frontend/index.html +0 -12
- package/template/frontend/package.json +0 -25
- package/template/frontend/pnpm-lock.yaml +0 -1119
- package/template/frontend/src/App.css +0 -389
- package/template/frontend/src/App.tsx +0 -38
- package/template/frontend/src/components/Header.tsx +0 -29
- package/template/frontend/src/main.tsx +0 -21
- package/template/frontend/src/pages/About/index.tsx +0 -39
- package/template/frontend/src/pages/Demo/index.tsx +0 -87
- package/template/frontend/src/pages/Home/index.tsx +0 -94
- package/template/frontend/src/utils/api.ts +0 -6
- package/template/frontend/src/vite-env.d.ts +0 -1
- package/template/frontend/tsconfig.json +0 -22
- package/template/frontend/tsconfig.node.json +0 -11
- package/template/frontend/vite.config.ts +0 -27
- package/template/package.json +0 -16
- package/template/pinme.toml +0 -18
- package/template/pnpm-workspace.yaml +0 -3
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
* {
|
|
2
|
-
margin: 0;
|
|
3
|
-
padding: 0;
|
|
4
|
-
box-sizing: border-box;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
body {
|
|
8
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
9
|
-
background: #f5f5f5;
|
|
10
|
-
color: #333;
|
|
11
|
-
min-height: 100vh;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.page {
|
|
15
|
-
max-width: 600px;
|
|
16
|
-
margin: 0 auto;
|
|
17
|
-
padding: 24px 16px;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.page-header {
|
|
21
|
-
display: flex;
|
|
22
|
-
justify-content: space-between;
|
|
23
|
-
align-items: center;
|
|
24
|
-
margin-bottom: 32px;
|
|
25
|
-
padding-bottom: 16px;
|
|
26
|
-
border-bottom: 1px solid #eee;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.page-header h1 {
|
|
30
|
-
font-size: 20px;
|
|
31
|
-
font-weight: 700;
|
|
32
|
-
color: #111;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.page-header nav {
|
|
36
|
-
display: flex;
|
|
37
|
-
gap: 16px;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.page-header nav a {
|
|
41
|
-
color: #666;
|
|
42
|
-
text-decoration: none;
|
|
43
|
-
font-size: 14px;
|
|
44
|
-
padding: 4px 8px;
|
|
45
|
-
border-radius: 4px;
|
|
46
|
-
transition: all 0.2s;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.page-header nav a:hover {
|
|
50
|
-
color: #111;
|
|
51
|
-
background: #eee;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.page-header nav a.active {
|
|
55
|
-
color: #fff;
|
|
56
|
-
background: #0070f3;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* Hero */
|
|
60
|
-
.hero {
|
|
61
|
-
text-align: center;
|
|
62
|
-
padding: 40px 0;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.hero h2 {
|
|
66
|
-
font-size: 28px;
|
|
67
|
-
font-weight: 700;
|
|
68
|
-
margin-bottom: 12px;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.hero p {
|
|
72
|
-
color: #666;
|
|
73
|
-
font-size: 16px;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/* Features */
|
|
77
|
-
.features {
|
|
78
|
-
display: grid;
|
|
79
|
-
grid-template-columns: repeat(3, 1fr);
|
|
80
|
-
gap: 16px;
|
|
81
|
-
margin: 32px 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.feature {
|
|
85
|
-
background: white;
|
|
86
|
-
padding: 20px;
|
|
87
|
-
border-radius: 12px;
|
|
88
|
-
text-align: center;
|
|
89
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.feature h3 {
|
|
93
|
-
font-size: 14px;
|
|
94
|
-
font-weight: 600;
|
|
95
|
-
margin-bottom: 8px;
|
|
96
|
-
color: #111;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.feature p {
|
|
100
|
-
font-size: 12px;
|
|
101
|
-
color: #666;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/* CTA */
|
|
105
|
-
.cta {
|
|
106
|
-
text-align: center;
|
|
107
|
-
margin: 32px 0;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.btn-primary {
|
|
111
|
-
display: inline-block;
|
|
112
|
-
padding: 12px 32px;
|
|
113
|
-
background: #0070f3;
|
|
114
|
-
color: white;
|
|
115
|
-
text-decoration: none;
|
|
116
|
-
border-radius: 8px;
|
|
117
|
-
font-weight: 600;
|
|
118
|
-
transition: background 0.2s;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.btn-primary:hover {
|
|
122
|
-
background: #0060df;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* API Info */
|
|
126
|
-
.api-info {
|
|
127
|
-
background: #f0f0f0;
|
|
128
|
-
padding: 12px;
|
|
129
|
-
border-radius: 6px;
|
|
130
|
-
margin-bottom: 24px;
|
|
131
|
-
overflow-x: auto;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.api-info code {
|
|
135
|
-
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
136
|
-
font-size: 12px;
|
|
137
|
-
color: #666;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/* Message Form */
|
|
141
|
-
.message-form {
|
|
142
|
-
display: flex;
|
|
143
|
-
gap: 12px;
|
|
144
|
-
margin-bottom: 24px;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.message-form input {
|
|
148
|
-
flex: 1;
|
|
149
|
-
padding: 12px 16px;
|
|
150
|
-
border: 1px solid #ddd;
|
|
151
|
-
border-radius: 8px;
|
|
152
|
-
font-size: 14px;
|
|
153
|
-
outline: none;
|
|
154
|
-
transition: border-color 0.2s;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.message-form input:focus {
|
|
158
|
-
border-color: #0070f3;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.message-form button {
|
|
162
|
-
padding: 12px 24px;
|
|
163
|
-
background: #0070f3;
|
|
164
|
-
color: white;
|
|
165
|
-
border: none;
|
|
166
|
-
border-radius: 8px;
|
|
167
|
-
font-size: 14px;
|
|
168
|
-
font-weight: 600;
|
|
169
|
-
cursor: pointer;
|
|
170
|
-
transition: background 0.2s;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.message-form button:hover:not(:disabled) {
|
|
174
|
-
background: #0060df;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.message-form button:disabled {
|
|
178
|
-
background: #ccc;
|
|
179
|
-
cursor: not-allowed;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/* Messages */
|
|
183
|
-
.messages h2 {
|
|
184
|
-
font-size: 18px;
|
|
185
|
-
font-weight: 600;
|
|
186
|
-
margin-bottom: 16px;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.messages .empty {
|
|
190
|
-
color: #999;
|
|
191
|
-
text-align: center;
|
|
192
|
-
padding: 24px;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.messages ul {
|
|
196
|
-
list-style: none;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.messages li {
|
|
200
|
-
background: white;
|
|
201
|
-
padding: 16px;
|
|
202
|
-
border-radius: 8px;
|
|
203
|
-
margin-bottom: 12px;
|
|
204
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.messages .content {
|
|
208
|
-
display: block;
|
|
209
|
-
margin-bottom: 8px;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.messages .time {
|
|
213
|
-
font-size: 12px;
|
|
214
|
-
color: #999;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/* About */
|
|
218
|
-
.about h2 {
|
|
219
|
-
font-size: 24px;
|
|
220
|
-
font-weight: 700;
|
|
221
|
-
margin-bottom: 16px;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.about h3 {
|
|
225
|
-
font-size: 16px;
|
|
226
|
-
font-weight: 600;
|
|
227
|
-
margin: 24px 0 12px;
|
|
228
|
-
color: #111;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
.about p {
|
|
232
|
-
color: #666;
|
|
233
|
-
line-height: 1.6;
|
|
234
|
-
margin-bottom: 16px;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.about ul {
|
|
238
|
-
list-style: none;
|
|
239
|
-
margin-bottom: 16px;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.about li {
|
|
243
|
-
color: #666;
|
|
244
|
-
padding: 8px 0;
|
|
245
|
-
border-bottom: 1px solid #eee;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.about li:last-child {
|
|
249
|
-
border-bottom: none;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.about li code {
|
|
253
|
-
background: #f0f0f0;
|
|
254
|
-
padding: 2px 6px;
|
|
255
|
-
border-radius: 4px;
|
|
256
|
-
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
257
|
-
font-size: 12px;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.about .links {
|
|
261
|
-
margin-top: 32px;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
.about .links a {
|
|
265
|
-
color: #0070f3;
|
|
266
|
-
text-decoration: none;
|
|
267
|
-
font-weight: 600;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.about .links a:hover {
|
|
271
|
-
text-decoration: underline;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/* Responsive */
|
|
275
|
-
@media (max-width: 480px) {
|
|
276
|
-
.features {
|
|
277
|
-
grid-template-columns: 1fr;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
.page-header {
|
|
281
|
-
flex-direction: column;
|
|
282
|
-
gap: 12px;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/* Demo */
|
|
287
|
-
.demo h2 {
|
|
288
|
-
font-size: 24px;
|
|
289
|
-
font-weight: 700;
|
|
290
|
-
margin-bottom: 8px;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.demo .desc {
|
|
294
|
-
color: #666;
|
|
295
|
-
margin-bottom: 24px;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.demo .api-box {
|
|
299
|
-
display: flex;
|
|
300
|
-
align-items: center;
|
|
301
|
-
background: #f0f0f0;
|
|
302
|
-
border-radius: 8px;
|
|
303
|
-
padding: 12px 16px;
|
|
304
|
-
margin-bottom: 16px;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
.demo .api-method {
|
|
308
|
-
background: #0070f3;
|
|
309
|
-
color: white;
|
|
310
|
-
padding: 4px 12px;
|
|
311
|
-
border-radius: 4px;
|
|
312
|
-
font-size: 12px;
|
|
313
|
-
font-weight: 600;
|
|
314
|
-
margin-right: 12px;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
.demo .api-url {
|
|
318
|
-
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
319
|
-
font-size: 14px;
|
|
320
|
-
color: #333;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.demo .btn-fetch {
|
|
324
|
-
width: 100%;
|
|
325
|
-
padding: 12px;
|
|
326
|
-
background: #0070f3;
|
|
327
|
-
color: white;
|
|
328
|
-
border: none;
|
|
329
|
-
border-radius: 8px;
|
|
330
|
-
font-size: 14px;
|
|
331
|
-
font-weight: 600;
|
|
332
|
-
cursor: pointer;
|
|
333
|
-
margin-bottom: 16px;
|
|
334
|
-
transition: background 0.2s;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
.demo .btn-fetch:hover:not(:disabled) {
|
|
338
|
-
background: #0060df;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
.demo .btn-fetch:disabled {
|
|
342
|
-
background: #ccc;
|
|
343
|
-
cursor: not-allowed;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
.demo .result {
|
|
347
|
-
background: white;
|
|
348
|
-
border-radius: 8px;
|
|
349
|
-
padding: 16px;
|
|
350
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
.demo .result h3 {
|
|
354
|
-
font-size: 14px;
|
|
355
|
-
font-weight: 600;
|
|
356
|
-
margin-bottom: 12px;
|
|
357
|
-
color: #333;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
.demo .result pre {
|
|
361
|
-
background: #f5f5f5;
|
|
362
|
-
padding: 12px;
|
|
363
|
-
border-radius: 6px;
|
|
364
|
-
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
365
|
-
font-size: 12px;
|
|
366
|
-
overflow-x: auto;
|
|
367
|
-
margin-bottom: 12px;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.demo .result.error {
|
|
371
|
-
background: #fff5f5;
|
|
372
|
-
color: #c00;
|
|
373
|
-
text-align: center;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
.demo .domain-result {
|
|
377
|
-
margin-top: 12px;
|
|
378
|
-
padding-top: 12px;
|
|
379
|
-
border-top: 1px solid #eee;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
.demo .domain-result span {
|
|
383
|
-
color: #666;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
.demo .domain-result a {
|
|
387
|
-
color: #0070f3;
|
|
388
|
-
font-weight: 600;
|
|
389
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { Link } from 'react-router-dom';
|
|
2
|
-
import Header from './components/Header';
|
|
3
|
-
|
|
4
|
-
export default function App() {
|
|
5
|
-
return (
|
|
6
|
-
<div className="page">
|
|
7
|
-
<Header />
|
|
8
|
-
|
|
9
|
-
<main>
|
|
10
|
-
<section className="hero">
|
|
11
|
-
<h2>欢迎使用 Pinme</h2>
|
|
12
|
-
<p>一站式全栈部署工具,前端上 IPFS,后端上 Worker</p>
|
|
13
|
-
</section>
|
|
14
|
-
|
|
15
|
-
<section className="features">
|
|
16
|
-
<div className="feature">
|
|
17
|
-
<h3>🚀 快速部署</h3>
|
|
18
|
-
<p>一行命令部署前端到 IPFS,永久不可篡改</p>
|
|
19
|
-
</div>
|
|
20
|
-
<div className="feature">
|
|
21
|
-
<h3>⚡ 极速响应</h3>
|
|
22
|
-
<p>Cloudflare 全球 CDN,边缘计算</p>
|
|
23
|
-
</div>
|
|
24
|
-
<div className="feature">
|
|
25
|
-
<h3>💾 持久存储</h3>
|
|
26
|
-
<p>D1 SQLite 数据库,轻松存储数据</p>
|
|
27
|
-
</div>
|
|
28
|
-
</section>
|
|
29
|
-
|
|
30
|
-
<section className="cta">
|
|
31
|
-
<Link to="/home" className="btn-primary">
|
|
32
|
-
开始使用 →
|
|
33
|
-
</Link>
|
|
34
|
-
</section>
|
|
35
|
-
</main>
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Link, useLocation } from 'react-router-dom';
|
|
2
|
-
|
|
3
|
-
const NAV_ITEMS = [
|
|
4
|
-
{ path: '/', label: '首页' },
|
|
5
|
-
{ path: '/home', label: '消息' },
|
|
6
|
-
{ path: '/demo', label: 'Demo' },
|
|
7
|
-
{ path: '/about', label: '关于' },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
export default function Header() {
|
|
11
|
-
const location = useLocation();
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<header className="page-header">
|
|
15
|
-
<h1>Pinme App</h1>
|
|
16
|
-
<nav>
|
|
17
|
-
{NAV_ITEMS.map((item) => (
|
|
18
|
-
<Link
|
|
19
|
-
key={item.path}
|
|
20
|
-
to={item.path}
|
|
21
|
-
className={location.pathname === item.path ? 'active' : ''}
|
|
22
|
-
>
|
|
23
|
-
{item.label}
|
|
24
|
-
</Link>
|
|
25
|
-
))}
|
|
26
|
-
</nav>
|
|
27
|
-
</header>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import ReactDOM from 'react-dom/client';
|
|
3
|
-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
4
|
-
import App from './App';
|
|
5
|
-
import Home from './pages/Home';
|
|
6
|
-
import About from './pages/About';
|
|
7
|
-
import Demo from './pages/Demo';
|
|
8
|
-
import './App.css';
|
|
9
|
-
|
|
10
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
11
|
-
<React.StrictMode>
|
|
12
|
-
<BrowserRouter>
|
|
13
|
-
<Routes>
|
|
14
|
-
<Route path="/" element={<App />} />
|
|
15
|
-
<Route path="/home" element={<Home />} />
|
|
16
|
-
<Route path="/about" element={<About />} />
|
|
17
|
-
<Route path="/demo" element={<Demo />} />
|
|
18
|
-
</Routes>
|
|
19
|
-
</BrowserRouter>
|
|
20
|
-
</React.StrictMode>
|
|
21
|
-
);
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import Header from '../../components/Header';
|
|
2
|
-
|
|
3
|
-
export default function About() {
|
|
4
|
-
return (
|
|
5
|
-
<div className="page">
|
|
6
|
-
<Header />
|
|
7
|
-
|
|
8
|
-
<main>
|
|
9
|
-
<section className="about">
|
|
10
|
-
<h2>关于 Pinme</h2>
|
|
11
|
-
<p>
|
|
12
|
-
Pinme 是一个一站式全栈部署工具,整合了去中心化静态托管(IPFS)和无服务器后端(Cloudflare Workers + D1 数据库)。
|
|
13
|
-
</p>
|
|
14
|
-
|
|
15
|
-
<h3>核心功能</h3>
|
|
16
|
-
<ul>
|
|
17
|
-
<li><code>pinme upload</code> - 前端静态文件 → IPFS,永久可访问</li>
|
|
18
|
-
<li><code>pinme worker deploy</code> - 后端 Worker → {`{name}.pinme.pro`}</li>
|
|
19
|
-
<li><code>pinme db migrate</code> - SQL 迁移文件 → D1 数据库</li>
|
|
20
|
-
</ul>
|
|
21
|
-
|
|
22
|
-
<h3>技术栈</h3>
|
|
23
|
-
<ul>
|
|
24
|
-
<li>前端: React + TypeScript + Vite</li>
|
|
25
|
-
<li>后端: Cloudflare Workers</li>
|
|
26
|
-
<li>数据库: Cloudflare D1 (SQLite)</li>
|
|
27
|
-
<li>部署: Pinme (IPFS + Workers)</li>
|
|
28
|
-
</ul>
|
|
29
|
-
|
|
30
|
-
<div className="links">
|
|
31
|
-
<a href="https://pinme.eth.limo" target="_blank" rel="noopener noreferrer">
|
|
32
|
-
访问 Pinme →
|
|
33
|
-
</a>
|
|
34
|
-
</div>
|
|
35
|
-
</section>
|
|
36
|
-
</main>
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import Header from '../../components/Header';
|
|
3
|
-
import { getApiUrl } from '../../utils/api';
|
|
4
|
-
|
|
5
|
-
interface RootDomainData {
|
|
6
|
-
code: number;
|
|
7
|
-
msg: string;
|
|
8
|
-
data?: {
|
|
9
|
-
domain: string;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default function Demo() {
|
|
14
|
-
const [loading, setLoading] = useState(false);
|
|
15
|
-
const [result, setResult] = useState<RootDomainData | null>(null);
|
|
16
|
-
const [error, setError] = useState<string | null>(null);
|
|
17
|
-
|
|
18
|
-
async function fetchRootDomain() {
|
|
19
|
-
setLoading(true);
|
|
20
|
-
setError(null);
|
|
21
|
-
try {
|
|
22
|
-
const res = await fetch(getApiUrl('/api/root-domain'));
|
|
23
|
-
const data = await res.json();
|
|
24
|
-
setResult(data);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
setError('请求失败,请稍后重试');
|
|
27
|
-
console.error(err);
|
|
28
|
-
} finally {
|
|
29
|
-
setLoading(false);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
fetchRootDomain();
|
|
35
|
-
}, []);
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="page">
|
|
39
|
-
<Header />
|
|
40
|
-
|
|
41
|
-
<main>
|
|
42
|
-
<section className="demo">
|
|
43
|
-
<h2>API Demo</h2>
|
|
44
|
-
<p className="desc">
|
|
45
|
-
示例:调用 Pinme API 获取用户的 Root Domain
|
|
46
|
-
</p>
|
|
47
|
-
|
|
48
|
-
<div className="api-box">
|
|
49
|
-
<div className="api-method">GET</div>
|
|
50
|
-
<div className="api-url">/api/root-domain</div>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<button
|
|
54
|
-
className="btn-fetch"
|
|
55
|
-
onClick={fetchRootDomain}
|
|
56
|
-
disabled={loading}
|
|
57
|
-
>
|
|
58
|
-
{loading ? '请求中...' : '重新获取'}
|
|
59
|
-
</button>
|
|
60
|
-
|
|
61
|
-
{error && (
|
|
62
|
-
<div className="result error">{error}</div>
|
|
63
|
-
)}
|
|
64
|
-
|
|
65
|
-
{result && (
|
|
66
|
-
<div className="result">
|
|
67
|
-
<h3>响应结果:</h3>
|
|
68
|
-
<pre>{JSON.stringify(result, null, 2)}</pre>
|
|
69
|
-
{result.data?.domain && (
|
|
70
|
-
<div className="domain-result">
|
|
71
|
-
<span>你的 Root Domain: </span>
|
|
72
|
-
<a
|
|
73
|
-
href={`https://${result.data.domain}`}
|
|
74
|
-
target="_blank"
|
|
75
|
-
rel="noopener noreferrer"
|
|
76
|
-
>
|
|
77
|
-
{result.data.domain} →
|
|
78
|
-
</a>
|
|
79
|
-
</div>
|
|
80
|
-
)}
|
|
81
|
-
</div>
|
|
82
|
-
)}
|
|
83
|
-
</section>
|
|
84
|
-
</main>
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import Header from '../../components/Header';
|
|
3
|
-
import { API, getApiUrl } from '../../utils/api';
|
|
4
|
-
|
|
5
|
-
interface Message {
|
|
6
|
-
id: number;
|
|
7
|
-
content: string;
|
|
8
|
-
created_at: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export default function Home() {
|
|
12
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
13
|
-
const [input, setInput] = useState('');
|
|
14
|
-
const [loading, setLoading] = useState(false);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
fetchMessages();
|
|
18
|
-
}, []);
|
|
19
|
-
|
|
20
|
-
async function fetchMessages() {
|
|
21
|
-
try {
|
|
22
|
-
const res = await fetch(getApiUrl('/api/hello'));
|
|
23
|
-
const data = await res.json();
|
|
24
|
-
setMessages(data.data || []);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.error('Failed to fetch messages:', error);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
31
|
-
e.preventDefault();
|
|
32
|
-
if (!input.trim()) return;
|
|
33
|
-
|
|
34
|
-
setLoading(true);
|
|
35
|
-
try {
|
|
36
|
-
const res = await fetch(getApiUrl('/api/messages'), {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'Content-Type': 'application/json' },
|
|
39
|
-
body: JSON.stringify({ message: input }),
|
|
40
|
-
});
|
|
41
|
-
if (res.ok) {
|
|
42
|
-
setInput('');
|
|
43
|
-
fetchMessages();
|
|
44
|
-
}
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.error('Failed to send message:', error);
|
|
47
|
-
} finally {
|
|
48
|
-
setLoading(false);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<div className="page">
|
|
54
|
-
<Header />
|
|
55
|
-
|
|
56
|
-
<main>
|
|
57
|
-
<section className="api-info">
|
|
58
|
-
<code>API: {API}</code>
|
|
59
|
-
</section>
|
|
60
|
-
|
|
61
|
-
<form onSubmit={handleSubmit} className="message-form">
|
|
62
|
-
<input
|
|
63
|
-
type="text"
|
|
64
|
-
value={input}
|
|
65
|
-
onChange={(e) => setInput(e.target.value)}
|
|
66
|
-
placeholder="输入消息..."
|
|
67
|
-
disabled={loading}
|
|
68
|
-
/>
|
|
69
|
-
<button type="submit" disabled={loading || !input.trim()}>
|
|
70
|
-
{loading ? '发送中...' : '发送'}
|
|
71
|
-
</button>
|
|
72
|
-
</form>
|
|
73
|
-
|
|
74
|
-
<section className="messages">
|
|
75
|
-
<h2>消息列表</h2>
|
|
76
|
-
{messages.length === 0 ? (
|
|
77
|
-
<p className="empty">暂无消息</p>
|
|
78
|
-
) : (
|
|
79
|
-
<ul>
|
|
80
|
-
{messages.map((msg) => (
|
|
81
|
-
<li key={msg.id}>
|
|
82
|
-
<span className="content">{msg.content}</span>
|
|
83
|
-
<span className="time">
|
|
84
|
-
{new Date(msg.created_at).toLocaleString()}
|
|
85
|
-
</span>
|
|
86
|
-
</li>
|
|
87
|
-
))}
|
|
88
|
-
</ul>
|
|
89
|
-
)}
|
|
90
|
-
</section>
|
|
91
|
-
</main>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|